]> git.basschouten.com Git - openhab-addons.git/blob
07628ad1f5096e2bfe4131a5cfa83580d45c0a85
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.handler;
14
15 import static org.openhab.binding.innogysmarthome.internal.InnogyBindingConstants.*;
16 import static org.openhab.binding.innogysmarthome.internal.client.Constants.API_URL_TOKEN;
17
18 import java.io.IOException;
19 import java.net.SocketTimeoutException;
20 import java.net.URI;
21 import java.time.format.DateTimeFormatter;
22 import java.time.format.FormatStyle;
23 import java.util.*;
24 import java.util.concurrent.*;
25
26 import org.apache.commons.lang.StringUtils;
27 import org.apache.commons.lang.exception.ExceptionUtils;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.client.HttpClient;
31 import org.openhab.binding.innogysmarthome.internal.InnogyWebSocket;
32 import org.openhab.binding.innogysmarthome.internal.client.InnogyClient;
33 import org.openhab.binding.innogysmarthome.internal.client.entity.action.ShutterAction;
34 import org.openhab.binding.innogysmarthome.internal.client.entity.capability.Capability;
35 import org.openhab.binding.innogysmarthome.internal.client.entity.device.Device;
36 import org.openhab.binding.innogysmarthome.internal.client.entity.device.DeviceConfig;
37 import org.openhab.binding.innogysmarthome.internal.client.entity.event.BaseEvent;
38 import org.openhab.binding.innogysmarthome.internal.client.entity.event.Event;
39 import org.openhab.binding.innogysmarthome.internal.client.entity.event.MessageEvent;
40 import org.openhab.binding.innogysmarthome.internal.client.entity.link.Link;
41 import org.openhab.binding.innogysmarthome.internal.client.entity.message.Message;
42 import org.openhab.binding.innogysmarthome.internal.client.exception.ApiException;
43 import org.openhab.binding.innogysmarthome.internal.client.exception.AuthenticationException;
44 import org.openhab.binding.innogysmarthome.internal.client.exception.ControllerOfflineException;
45 import org.openhab.binding.innogysmarthome.internal.client.exception.InvalidActionTriggeredException;
46 import org.openhab.binding.innogysmarthome.internal.client.exception.RemoteAccessNotAllowedException;
47 import org.openhab.binding.innogysmarthome.internal.client.exception.SessionExistsException;
48 import org.openhab.binding.innogysmarthome.internal.discovery.InnogyDeviceDiscoveryService;
49 import org.openhab.binding.innogysmarthome.internal.listener.DeviceStatusListener;
50 import org.openhab.binding.innogysmarthome.internal.listener.EventListener;
51 import org.openhab.binding.innogysmarthome.internal.manager.DeviceStructureManager;
52 import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
53 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
54 import org.openhab.core.auth.client.oauth2.OAuthClientService;
55 import org.openhab.core.auth.client.oauth2.OAuthException;
56 import org.openhab.core.auth.client.oauth2.OAuthFactory;
57 import org.openhab.core.auth.client.oauth2.OAuthResponseException;
58 import org.openhab.core.config.core.Configuration;
59 import org.openhab.core.library.types.DecimalType;
60 import org.openhab.core.thing.Bridge;
61 import org.openhab.core.thing.ChannelUID;
62 import org.openhab.core.thing.Thing;
63 import org.openhab.core.thing.ThingStatus;
64 import org.openhab.core.thing.ThingStatusDetail;
65 import org.openhab.core.thing.ThingTypeUID;
66 import org.openhab.core.thing.binding.BaseBridgeHandler;
67 import org.openhab.core.thing.binding.ThingHandlerService;
68 import org.openhab.core.types.Command;
69 import org.slf4j.Logger;
70 import org.slf4j.LoggerFactory;
71
72 import com.google.gson.Gson;
73
74 /**
75  * The {@link InnogyBridgeHandler} is responsible for handling the innogy SmartHome controller including the connection
76  * to the innogy backend for all communications with the innogy {@link Device}s.
77  * <p/>
78  * It implements the {@link AccessTokenRefreshListener} to handle updates of the oauth2 tokens and the
79  * {@link EventListener} to handle {@link Event}s, that are received by the {@link InnogyWebSocket}.
80  * <p/>
81  * The {@link Device}s are organized by the {@link DeviceStructureManager}, which is also responsible for the connection
82  * to the innogy SmartHome webservice via the {@link InnogyClient}.
83  *
84  * @author Oliver Kuhl - Initial contribution
85  * @author Hilbrand Bouwkamp - Refactored to use openHAB http and oauth2 libraries
86  */
87 @NonNullByDefault
88 public class InnogyBridgeHandler extends BaseBridgeHandler
89         implements AccessTokenRefreshListener, EventListener, DeviceStatusListener {
90
91     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);
92
93     private final Logger logger = LoggerFactory.getLogger(InnogyBridgeHandler.class);
94     private final Gson gson = new Gson();
95     private final Object lock = new Object();
96     private final Set<DeviceStatusListener> deviceStatusListeners = new CopyOnWriteArraySet<>();
97     private final OAuthFactory oAuthFactory;
98     private final HttpClient httpClient;
99
100     private @Nullable InnogyClient client;
101     private @Nullable InnogyWebSocket webSocket;
102     private @Nullable DeviceStructureManager deviceStructMan;
103     private @Nullable String bridgeId;
104     private @Nullable ScheduledFuture<?> reinitJob;
105     private @NonNullByDefault({}) InnogyBridgeConfiguration bridgeConfiguration;
106     private @Nullable OAuthClientService oAuthService;
107
108     /**
109      * Constructs a new {@link InnogyBridgeHandler}.
110      *
111      * @param bridge Bridge thing to be used by this handler
112      * @param oAuthFactory Factory class to get OAuth2 service
113      * @param httpClient httpclient instance
114      */
115     public InnogyBridgeHandler(final Bridge bridge, final OAuthFactory oAuthFactory, final HttpClient httpClient) {
116         super(bridge);
117         this.oAuthFactory = oAuthFactory;
118         this.httpClient = httpClient;
119     }
120
121     @Override
122     public void handleCommand(final ChannelUID channelUID, final Command command) {
123         // not needed
124     }
125
126     @Override
127     public Collection<Class<? extends ThingHandlerService>> getServices() {
128         return Collections.singleton(InnogyDeviceDiscoveryService.class);
129     }
130
131     @Override
132     public void initialize() {
133         logger.debug("Initializing innogy SmartHome BridgeHandler...");
134         final InnogyBridgeConfiguration bridgeConfiguration = getConfigAs(InnogyBridgeConfiguration.class);
135         if (checkConfig(bridgeConfiguration)) {
136             this.bridgeConfiguration = bridgeConfiguration;
137             getScheduler().execute(this::initializeClient);
138         }
139     }
140
141     /**
142      * Checks bridge configuration. If configuration is valid returns true.
143      *
144      * @return true if the configuration if valid
145      */
146     private boolean checkConfig(final InnogyBridgeConfiguration bridgeConfiguration) {
147         if (BRAND_INNOGY_SMARTHOME.equals(bridgeConfiguration.brand)) {
148             return true;
149         } else {
150             logger.debug("Invalid brand '{}'. Make sure to select a brand in the SHC thing configuration!",
151                     bridgeConfiguration.brand);
152             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid brand '"
153                     + bridgeConfiguration.brand + "'. Make sure to select a brand in the SHC thing configuration!");
154             return false;
155         }
156     }
157
158     /**
159      * Initializes the services and InnogyClient.
160      */
161     private void initializeClient() {
162         final OAuthClientService oAuthService = oAuthFactory.createOAuthClientService(thing.getUID().getAsString(),
163                 API_URL_TOKEN, API_URL_TOKEN, bridgeConfiguration.clientId, bridgeConfiguration.clientSecret, null,
164                 true);
165         this.oAuthService = oAuthService;
166
167         if (checkOnAuthCode()) {
168             final InnogyClient localClient = createInnogyClient(oAuthService, httpClient);
169             client = localClient;
170             deviceStructMan = new DeviceStructureManager(localClient);
171             oAuthService.addAccessTokenRefreshListener(this);
172             registerDeviceStatusListener(InnogyBridgeHandler.this);
173             scheduleRestartClient(false);
174         }
175     }
176
177     /**
178      * Fetches the OAuth2 tokens from innogy SmartHome service if the auth code is set in the configuration and if
179      * successful removes the auth code. Returns true if the auth code was not set or if the authcode was successfully
180      * used to get a new refresh and access token.
181      *
182      * @return true if success
183      */
184     private boolean checkOnAuthCode() {
185         if (StringUtils.isNotBlank(bridgeConfiguration.authcode)) {
186             logger.debug("Trying to get access and refresh tokens");
187             try {
188                 oAuthService.getAccessTokenResponseByAuthorizationCode(bridgeConfiguration.authcode,
189                         bridgeConfiguration.redirectUrl);
190                 final Configuration configuration = editConfiguration();
191                 configuration.put(CONFIG_AUTH_CODE, "");
192                 updateConfiguration(configuration);
193             } catch (IOException | OAuthException | OAuthResponseException e) {
194                 logger.debug("Error fetching access tokens. Invalid authcode! Please generate a new one. Detail: {}",
195                         e.getMessage());
196                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
197                         "Cannot connect to innogy SmartHome service. Please set auth-code!");
198                 return false;
199             }
200         }
201         return true;
202     }
203
204     /**
205      * Initializes the client and connects to the innogy SmartHome service via Client API. Based on the provided
206      * {@Link Configuration} while constructing {@Link InnogyClient}, the given oauth2 access and refresh tokens are
207      * used or - if not yet available - new tokens are fetched from the service using the provided auth code.
208      */
209     private void startClient() {
210         try {
211             logger.debug("Initializing innogy SmartHome client...");
212             final InnogyClient localClient = this.client;
213             if (localClient != null) {
214                 localClient.refreshStatus();
215             }
216         } catch (AuthenticationException | ApiException | IOException e) {
217             if (handleClientException(e)) {
218                 // If exception could not be handled properly it's no use to continue so we won't continue start
219                 logger.debug("Error initializing innogy SmartHome client.", e);
220                 return;
221             }
222         }
223         final DeviceStructureManager deviceStructMan = this.deviceStructMan;
224         if (deviceStructMan == null) {
225             return;
226         }
227         try {
228             deviceStructMan.refreshDevices();
229         } catch (IOException | ApiException | AuthenticationException e) {
230             if (handleClientException(e)) {
231                 // If exception could not be handled properly it's no use to continue so we won't continue start
232                 logger.debug("Error starting device structure manager.", e);
233                 return;
234             }
235         }
236
237         Device bridgeDevice = deviceStructMan.getBridgeDevice();
238         if (bridgeDevice == null) {
239             logger.debug("Failed to get bridge device, re-scheduling startClient.");
240             scheduleRestartClient(true);
241             return;
242         }
243         setBridgeProperties(bridgeDevice);
244         bridgeId = bridgeDevice.getId();
245         startWebsocket();
246     }
247
248     /**
249      * Start the websocket connection for receiving permanent update {@link Event}s from the innogy API.
250      */
251     private void startWebsocket() {
252         try {
253             InnogyWebSocket localWebSocket = createWebSocket();
254
255             if (this.webSocket != null && this.webSocket.isRunning()) {
256                 this.webSocket.stop();
257                 this.webSocket = null;
258             }
259
260             logger.debug("Starting innogy websocket.");
261             this.webSocket = localWebSocket;
262             localWebSocket.start();
263             updateStatus(ThingStatus.ONLINE);
264         } catch (final Exception e) { // Catch Exception because websocket start throws Exception
265             logger.warn("Error starting websocket.", e);
266             handleClientException(e);
267         }
268     }
269
270     InnogyWebSocket createWebSocket() throws IOException, AuthenticationException {
271         final AccessTokenResponse accessTokenResponse = client.getAccessTokenResponse();
272         final String webSocketUrl = WEBSOCKET_API_URL_EVENTS.replace("{token}", accessTokenResponse.getAccessToken());
273
274         logger.debug("WebSocket URL: {}...{}", webSocketUrl.substring(0, 70),
275                 webSocketUrl.substring(webSocketUrl.length() - 10));
276
277         return new InnogyWebSocket(this, URI.create(webSocketUrl), bridgeConfiguration.websocketidletimeout * 1000);
278     }
279
280     @Override
281     public void onAccessTokenResponse(final AccessTokenResponse credential) {
282         scheduleRestartClient(true);
283     }
284
285     /**
286      * Schedules a re-initialization in the given future.
287      *
288      * @param delayed when it is scheduled delayed, it starts with a delay of
289      *            {@link org.openhab.binding.innogysmarthome.internal.InnogyBindingConstants#REINITIALIZE_DELAY_SECONDS}
290      *            seconds,
291      *            otherwise it starts directly
292      */
293     private synchronized void scheduleRestartClient(final boolean delayed) {
294         @Nullable
295         final ScheduledFuture<?> localReinitJob = reinitJob;
296
297         if (localReinitJob != null && isAlreadyScheduled(localReinitJob)) {
298             logger.debug("Scheduling reinitialize - ignored: already triggered in {} seconds.",
299                     localReinitJob.getDelay(TimeUnit.SECONDS));
300             return;
301         }
302
303         final long seconds = delayed ? REINITIALIZE_DELAY_SECONDS : 0;
304         logger.debug("Scheduling reinitialize in {} seconds.", seconds);
305         reinitJob = getScheduler().schedule(this::startClient, seconds, TimeUnit.SECONDS);
306     }
307
308     private void setBridgeProperties(final Device bridgeDevice) {
309         final DeviceConfig config = bridgeDevice.getConfig();
310
311         logger.debug("Setting Bridge Device Properties for Bridge of type '{}' with ID '{}'", config.getName(),
312                 bridgeDevice.getId());
313         final Map<String, String> properties = editProperties();
314
315         setPropertyIfPresent(Thing.PROPERTY_VENDOR, bridgeDevice.getManufacturer(), properties);
316         setPropertyIfPresent(Thing.PROPERTY_SERIAL_NUMBER, bridgeDevice.getSerialnumber(), properties);
317         setPropertyIfPresent(PROPERTY_ID, bridgeDevice.getId(), properties);
318         setPropertyIfPresent(Thing.PROPERTY_FIRMWARE_VERSION, config.getFirmwareVersion(), properties);
319         setPropertyIfPresent(Thing.PROPERTY_HARDWARE_VERSION, config.getHardwareVersion(), properties);
320         setPropertyIfPresent(PROPERTY_SOFTWARE_VERSION, config.getSoftwareVersion(), properties);
321         setPropertyIfPresent(PROPERTY_IP_ADDRESS, config.getIPAddress(), properties);
322         setPropertyIfPresent(Thing.PROPERTY_MAC_ADDRESS, config.getMACAddress(), properties);
323         if (config.getRegistrationTime() != null) {
324             properties.put(PROPERTY_REGISTRATION_TIME,
325                     config.getRegistrationTime().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)));
326         }
327         setPropertyIfPresent(PROPERTY_CONFIGURATION_STATE, config.getConfigurationState(), properties);
328         setPropertyIfPresent(PROPERTY_SHC_TYPE, bridgeDevice.getType(), properties);
329         setPropertyIfPresent(PROPERTY_TIME_ZONE, config.getTimeZone(), properties);
330         setPropertyIfPresent(PROPERTY_PROTOCOL_ID, config.getProtocolId(), properties);
331         setPropertyIfPresent(PROPERTY_GEOLOCATION, config.getGeoLocation(), properties);
332         setPropertyIfPresent(PROPERTY_CURRENT_UTC_OFFSET, config.getCurrentUTCOffset(), properties);
333         setPropertyIfPresent(PROPERTY_BACKEND_CONNECTION_MONITORED, config.getBackendConnectionMonitored(), properties);
334         setPropertyIfPresent(PROPERTY_RFCOM_FAILURE_NOTIFICATION, config.getRFCommFailureNotification(), properties);
335         updateProperties(properties);
336     }
337
338     private void setPropertyIfPresent(final String key, final @Nullable Object data,
339             final Map<String, String> properties) {
340         if (data != null) {
341             properties.put(key, data instanceof String ? (String) data : data.toString());
342         }
343     }
344
345     @Override
346     public void dispose() {
347         logger.debug("Disposing innogy SmartHome bridge handler '{}'", getThing().getUID().getId());
348         unregisterDeviceStatusListener(this);
349         cancelReinitJob();
350         if (webSocket != null) {
351             webSocket.stop();
352             webSocket = null;
353         }
354         client = null;
355         deviceStructMan = null;
356
357         super.dispose();
358         logger.debug("innogy SmartHome bridge handler shut down.");
359     }
360
361     private synchronized void cancelReinitJob() {
362         ScheduledFuture<?> reinitJob = this.reinitJob;
363
364         if (reinitJob != null) {
365             reinitJob.cancel(true);
366             this.reinitJob = null;
367         }
368     }
369
370     /**
371      * Registers a {@link DeviceStatusListener}.
372      *
373      * @param deviceStatusListener
374      * @return true, if successful
375      */
376     public boolean registerDeviceStatusListener(final DeviceStatusListener deviceStatusListener) {
377         return deviceStatusListeners.add(deviceStatusListener);
378     }
379
380     /**
381      * Unregisters a {@link DeviceStatusListener}.
382      *
383      * @param deviceStatusListener
384      * @return true, if successful
385      */
386     public boolean unregisterDeviceStatusListener(final DeviceStatusListener deviceStatusListener) {
387         return deviceStatusListeners.remove(deviceStatusListener);
388     }
389
390     /**
391      * Loads a Collection of {@link Device}s from the bridge and returns them.
392      *
393      * @return a Collection of {@link Device}s
394      */
395     public Collection<Device> loadDevices() {
396         final DeviceStructureManager deviceStructMan = this.deviceStructMan;
397         final Collection<Device> devices;
398
399         if (deviceStructMan == null) {
400             devices = Collections.emptyList();
401         } else {
402             devices = deviceStructMan.getDeviceList();
403         }
404         return devices;
405     }
406
407     /**
408      * Returns the {@link Device} with the given deviceId.
409      *
410      * @param deviceId
411      * @return {@link Device} or null, if it does not exist or no {@link DeviceStructureManager} is available
412      */
413     public @Nullable Device getDeviceById(final String deviceId) {
414         if (deviceStructMan != null) {
415             return deviceStructMan.getDeviceById(deviceId);
416         }
417         return null;
418     }
419
420     /**
421      * Refreshes the {@link Device} with the given id, by reloading the full device from the innogy webservice.
422      *
423      * @param deviceId
424      * @return the {@link Device} or null, if it does not exist or no {@link DeviceStructureManager} is available
425      */
426     public @Nullable Device refreshDevice(final String deviceId) {
427         final DeviceStructureManager deviceStructMan = this.deviceStructMan;
428         if (deviceStructMan == null) {
429             return null;
430         }
431
432         Device device = null;
433         try {
434             deviceStructMan.refreshDevice(deviceId);
435             device = deviceStructMan.getDeviceById(deviceId);
436         } catch (IOException | ApiException | AuthenticationException e) {
437             handleClientException(e);
438         }
439         return device;
440     }
441
442     @Override
443     public void onDeviceStateChanged(final Device device) {
444         synchronized (this.lock) {
445             if (!bridgeId.equals(device.getId())) {
446                 logger.trace("DeviceId {} not relevant for this handler (responsible for id {})", device.getId(),
447                         bridgeId);
448                 return;
449             }
450
451             logger.debug("onDeviceStateChanged called with device {}/{}", device.getConfig().getName(), device.getId());
452
453             // DEVICE STATES
454             if (device.hasDeviceState()) {
455                 final Double cpuUsage = device.getDeviceState().getState().getCpuUsage().getValue();
456                 if (cpuUsage != null) {
457                     logger.debug("-> CPU usage state: {}", cpuUsage);
458                     updateState(CHANNEL_CPU, new DecimalType(cpuUsage));
459                 }
460                 final Double diskUsage = device.getDeviceState().getState().getDiskUsage().getValue();
461                 if (diskUsage != null) {
462                     logger.debug("-> Disk usage state: {}", diskUsage);
463                     updateState(CHANNEL_DISK, new DecimalType(diskUsage));
464                 }
465                 final Double memoryUsage = device.getDeviceState().getState().getMemoryUsage().getValue();
466                 if (memoryUsage != null) {
467                     logger.debug("-> Memory usage state: {}", memoryUsage);
468                     updateState(CHANNEL_MEMORY, new DecimalType(memoryUsage));
469                 }
470
471             }
472
473         }
474     }
475
476     @Override
477     public void onDeviceStateChanged(final Device device, final Event event) {
478         synchronized (this.lock) {
479             if (!bridgeId.equals(device.getId())) {
480                 logger.trace("DeviceId {} not relevant for this handler (responsible for id {})", device.getId(),
481                         bridgeId);
482                 return;
483             }
484
485             logger.trace("DeviceId {} relevant for this handler.", device.getId());
486
487             if (event.isLinkedtoDevice() && DEVICE_SHCA.equals(device.getType())) {
488                 device.getDeviceState().getState().getCpuUsage().setValue(event.getProperties().getCpuUsage());
489                 device.getDeviceState().getState().getDiskUsage().setValue(event.getProperties().getDiskUsage());
490                 device.getDeviceState().getState().getMemoryUsage().setValue(event.getProperties().getMemoryUsage());
491                 onDeviceStateChanged(device);
492             }
493         }
494     }
495
496     @Override
497     public void onEvent(final String msg) {
498         logger.trace("onEvent called. Msg: {}", msg);
499
500         try {
501             final BaseEvent be = gson.fromJson(msg, BaseEvent.class);
502             logger.debug("Event no {} found. Type: {}", be.getSequenceNumber(), be.getType());
503             if (!BaseEvent.SUPPORTED_EVENT_TYPES.contains(be.getType())) {
504                 logger.debug("Event type {} not supported. Skipping...", be.getType());
505             } else {
506                 final Event event = gson.fromJson(msg, Event.class);
507
508                 switch (event.getType()) {
509                     case BaseEvent.TYPE_STATE_CHANGED:
510                     case BaseEvent.TYPE_BUTTON_PRESSED:
511                         handleStateChangedEvent(event);
512                         break;
513
514                     case BaseEvent.TYPE_DISCONNECT:
515                         logger.debug("Websocket disconnected.");
516                         scheduleRestartClient(true);
517                         break;
518
519                     case BaseEvent.TYPE_CONFIGURATION_CHANGED:
520                         if (client.getConfigVersion().equals(event.getConfigurationVersion().toString())) {
521                             logger.debug(
522                                     "Ignored configuration changed event with version '{}' as current version is '{}' the same.",
523                                     event.getConfigurationVersion(), client.getConfigVersion());
524                         } else {
525                             logger.info("Configuration changed from version {} to {}. Restarting innogy binding...",
526                                     client.getConfigVersion(), event.getConfigurationVersion());
527                             scheduleRestartClient(false);
528                         }
529                         break;
530
531                     case BaseEvent.TYPE_CONTROLLER_CONNECTIVITY_CHANGED:
532                         handleControllerConnectivityChangedEvent(event);
533                         break;
534
535                     case BaseEvent.TYPE_NEW_MESSAGE_RECEIVED:
536                     case BaseEvent.TYPE_MESSAGE_CREATED:
537                         final MessageEvent messageEvent = gson.fromJson(msg, MessageEvent.class);
538                         handleNewMessageReceivedEvent(Objects.requireNonNull(messageEvent));
539                         break;
540
541                     case BaseEvent.TYPE_MESSAGE_DELETED:
542                         handleMessageDeletedEvent(event);
543                         break;
544
545                     default:
546                         logger.debug("Unsupported eventtype {}.", event.getType());
547                         break;
548                 }
549             }
550         } catch (IOException | ApiException | AuthenticationException | RuntimeException e) {
551             logger.debug("Error with Event: {}", e.getMessage(), e);
552             handleClientException(e);
553         }
554     }
555
556     @Override
557     public void onError(final Throwable cause) {
558         if (cause instanceof Exception) {
559             handleClientException((Exception) cause);
560         }
561     }
562
563     /**
564      * Handles the event that occurs, when the state of a device (like reachability) or a capability (like a temperature
565      * value) has changed.
566      *
567      * @param event
568      * @throws ApiException
569      * @throws IOException
570      * @throws AuthenticationException
571      */
572     public void handleStateChangedEvent(final Event event) throws ApiException, IOException, AuthenticationException {
573         final DeviceStructureManager deviceStructMan = this.deviceStructMan;
574         if (deviceStructMan == null) {
575             return;
576         }
577
578         // CAPABILITY
579         if (event.isLinkedtoCapability()) {
580             logger.trace("Event is linked to capability");
581             final Device device = deviceStructMan.getDeviceByCapabilityId(event.getSourceId());
582             if (device != null) {
583                 for (final DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
584                     deviceStatusListener.onDeviceStateChanged(device, event);
585                 }
586             } else {
587                 logger.debug("Unknown/unsupported device for capability {}.", event.getSource());
588             }
589
590             // DEVICE
591         } else if (event.isLinkedtoDevice()) {
592             logger.trace("Event is linked to device");
593
594             if (!event.getSourceId().equals(deviceStructMan.getBridgeDevice().getId())) {
595                 deviceStructMan.refreshDevice(event.getSourceId());
596             }
597             final Device device = deviceStructMan.getDeviceById(event.getSourceId());
598             if (device != null) {
599                 for (final DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
600                     deviceStatusListener.onDeviceStateChanged(device, event);
601                 }
602             } else {
603                 logger.debug("Unknown/unsupported device {}.", event.getSourceId());
604             }
605
606         } else {
607             logger.debug("link type {} not supported (yet?)", event.getSourceLinkType());
608         }
609     }
610
611     /**
612      * Handles the event that occurs, when the connectivity of the bridge has changed.
613      *
614      * @param event
615      * @throws ApiException
616      * @throws IOException
617      * @throws AuthenticationException
618      */
619     public void handleControllerConnectivityChangedEvent(final Event event)
620             throws ApiException, IOException, AuthenticationException {
621         final DeviceStructureManager deviceStructMan = this.deviceStructMan;
622         if (deviceStructMan == null) {
623             return;
624         }
625         final Boolean connected = event.getIsConnected();
626         if (connected != null) {
627             logger.debug("SmartHome Controller connectivity changed to {}.", connected ? "online" : "offline");
628             if (connected) {
629                 deviceStructMan.refreshDevices();
630                 updateStatus(ThingStatus.ONLINE);
631             } else {
632                 updateStatus(ThingStatus.OFFLINE);
633             }
634         } else {
635             logger.warn("isConnected property missing in event! (returned null)");
636         }
637     }
638
639     /**
640      * Handles the event that occurs, when a new message was received. Currently only handles low battery messages.
641      *
642      * @param event
643      * @throws ApiException
644      * @throws IOException
645      * @throws AuthenticationException
646      */
647     public void handleNewMessageReceivedEvent(final MessageEvent event)
648             throws ApiException, IOException, AuthenticationException {
649         final DeviceStructureManager deviceStructMan = this.deviceStructMan;
650         if (deviceStructMan == null) {
651             return;
652         }
653         final Message message = event.getMessage();
654         if (logger.isTraceEnabled()) {
655             logger.trace("Message: {}", gson.toJson(message));
656             logger.trace("Messagetype: {}", message.getType());
657         }
658         if (Message.TYPE_DEVICE_LOW_BATTERY.equals(message.getType()) && message.getDevices() != null) {
659             for (final String link : message.getDevices()) {
660                 deviceStructMan.refreshDevice(Link.getId(link));
661                 final Device device = deviceStructMan.getDeviceById(Link.getId(link));
662                 if (device != null) {
663                     for (final DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
664                         deviceStatusListener.onDeviceStateChanged(device);
665                     }
666                 } else {
667                     logger.debug("Unknown/unsupported device {}.", event.getSourceId());
668                 }
669             }
670         } else {
671             logger.debug("Message received event not yet implemented for Messagetype {}.", message.getType());
672         }
673     }
674
675     /**
676      * Handle the event that occurs, when a message was deleted. In case of a low battery message this means, that the
677      * device is back to normal. Currently, only messages linked to devices are handled by refreshing the device data
678      * and informing the {@link InnogyDeviceHandler} about the changed device.
679      *
680      * @param event
681      * @throws ApiException
682      * @throws IOException
683      * @throws AuthenticationException
684      */
685     public void handleMessageDeletedEvent(final Event event) throws ApiException, IOException, AuthenticationException {
686         final DeviceStructureManager deviceStructMan = this.deviceStructMan;
687         if (deviceStructMan == null) {
688             return;
689         }
690         final String messageId = event.getData().getId();
691
692         logger.debug("handleMessageDeletedEvent with messageId '{}'", messageId);
693         Device device = deviceStructMan.getDeviceWithMessageId(messageId);
694
695         if (device != null) {
696             String id = device.getId();
697             deviceStructMan.refreshDevice(id);
698             device = deviceStructMan.getDeviceById(id);
699             if (device != null) {
700                 for (final DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
701                     deviceStatusListener.onDeviceStateChanged(device);
702                 }
703             } else {
704                 logger.debug("No device with id {} found after refresh.", id);
705             }
706         } else {
707             logger.debug("No device found with message id {}.", messageId);
708         }
709     }
710
711     @Override
712     public void connectionClosed() {
713         scheduleRestartClient(true);
714     }
715
716     /**
717      * Sends the command to switch the {@link Device} with the given id to the new state. Is called by the
718      * {@link InnogyDeviceHandler} for switch devices like the VariableActuator, PSS, PSSO or ISS2.
719      *
720      * @param deviceId
721      * @param state
722      */
723     public void commandSwitchDevice(final String deviceId, final boolean state) {
724         final DeviceStructureManager deviceStructMan = this.deviceStructMan;
725         if (deviceStructMan == null) {
726             return;
727         }
728         try {
729             // VariableActuator
730             final String deviceType = deviceStructMan.getDeviceById(deviceId).getType();
731             if (DEVICE_VARIABLE_ACTUATOR.equals(deviceType)) {
732                 final String capabilityId = deviceStructMan.getCapabilityId(deviceId, Capability.TYPE_VARIABLEACTUATOR);
733                 if (capabilityId == null) {
734                     return;
735                 }
736                 client.setVariableActuatorState(capabilityId, state);
737
738                 // PSS / PSSO / ISS2
739             } else if (DEVICE_PSS.equals(deviceType) || DEVICE_PSSO.equals(deviceType)
740                     || DEVICE_ISS2.equals(deviceType)) {
741                 final String capabilityId = deviceStructMan.getCapabilityId(deviceId, Capability.TYPE_SWITCHACTUATOR);
742                 if (capabilityId == null) {
743                     return;
744                 }
745                 client.setSwitchActuatorState(capabilityId, state);
746             }
747         } catch (IOException | ApiException | AuthenticationException e) {
748             handleClientException(e);
749         }
750     }
751
752     /**
753      * Sends the command to update the point temperature of the {@link Device} with the given deviceId. Is called by the
754      * {@link InnogyDeviceHandler} for thermostat {@link Device}s like RST or WRT.
755      *
756      * @param deviceId
757      * @param pointTemperature
758      */
759     public void commandUpdatePointTemperature(final String deviceId, final double pointTemperature) {
760         final DeviceStructureManager deviceStructMan = this.deviceStructMan;
761         if (deviceStructMan == null) {
762             return;
763         }
764         try {
765             final String capabilityId = deviceStructMan.getCapabilityId(deviceId, Capability.TYPE_THERMOSTATACTUATOR);
766             if (capabilityId == null) {
767                 return;
768             }
769             client.setPointTemperatureState(capabilityId, pointTemperature);
770         } catch (IOException | ApiException | AuthenticationException e) {
771             handleClientException(e);
772         }
773     }
774
775     /**
776      * Sends the command to turn the alarm of the {@link Device} with the given id on or off. Is called by the
777      * {@link InnogyDeviceHandler} for smoke detector {@link Device}s like WSD or WSD2.
778      *
779      * @param deviceId
780      * @param alarmState
781      */
782     public void commandSwitchAlarm(final String deviceId, final boolean alarmState) {
783         final DeviceStructureManager deviceStructMan = this.deviceStructMan;
784         if (deviceStructMan == null) {
785             return;
786         }
787         try {
788             final String capabilityId = deviceStructMan.getCapabilityId(deviceId, Capability.TYPE_ALARMACTUATOR);
789             if (capabilityId == null) {
790                 return;
791             }
792             client.setAlarmActuatorState(capabilityId, alarmState);
793         } catch (IOException | ApiException | AuthenticationException e) {
794             handleClientException(e);
795         }
796     }
797
798     /**
799      * Sends the command to set the operation mode of the {@link Device} with the given deviceId to auto (or manual, if
800      * false). Is called by the {@link InnogyDeviceHandler} for thermostat {@link Device}s like RST.
801      *
802      * @param deviceId
803      * @param autoMode true activates the automatic mode, false the manual mode.
804      */
805     public void commandSetOperationMode(final String deviceId, final boolean autoMode) {
806         final DeviceStructureManager deviceStructMan = this.deviceStructMan;
807         if (deviceStructMan == null) {
808             return;
809         }
810         try {
811             final String capabilityId = deviceStructMan.getCapabilityId(deviceId, Capability.TYPE_THERMOSTATACTUATOR);
812             if (capabilityId == null) {
813                 return;
814             }
815             client.setOperationMode(capabilityId, autoMode);
816         } catch (IOException | ApiException | AuthenticationException e) {
817             handleClientException(e);
818         }
819     }
820
821     /**
822      * Sends the command to set the dimm level of the {@link Device} with the given id. Is called by the
823      * {@link InnogyDeviceHandler} for {@link Device}s like ISD2 or PSD.
824      *
825      * @param deviceId
826      * @param dimLevel
827      */
828     public void commandSetDimmLevel(final String deviceId, final int dimLevel) {
829         final DeviceStructureManager deviceStructMan = this.deviceStructMan;
830         if (deviceStructMan == null) {
831             return;
832         }
833         try {
834             final String capabilityId = deviceStructMan.getCapabilityId(deviceId, Capability.TYPE_DIMMERACTUATOR);
835             if (capabilityId == null) {
836                 return;
837             }
838             client.setDimmerActuatorState(capabilityId, dimLevel);
839         } catch (IOException | ApiException | AuthenticationException e) {
840             handleClientException(e);
841         }
842     }
843
844     /**
845      * Sends the command to set the rollershutter level of the {@link Device} with the given id. Is called by the
846      * {@link InnogyDeviceHandler} for {@link Device}s like ISR2.
847      *
848      * @param deviceId
849      * @param rollerSchutterLevel
850      */
851     public void commandSetRollerShutterLevel(final String deviceId, final int rollerSchutterLevel) {
852         final DeviceStructureManager deviceStructMan = this.deviceStructMan;
853         if (deviceStructMan == null) {
854             return;
855         }
856         try {
857             final String capabilityId = deviceStructMan.getCapabilityId(deviceId,
858                     Capability.TYPE_ROLLERSHUTTERACTUATOR);
859             if (capabilityId == null) {
860                 return;
861             }
862             client.setRollerShutterActuatorState(capabilityId, rollerSchutterLevel);
863         } catch (IOException | ApiException | AuthenticationException e) {
864             handleClientException(e);
865         }
866     }
867
868     /**
869      * Sends the command to start or stop moving the rollershutter (ISR2) in a specified direction
870      * 
871      * @param deviceId
872      * @param action
873      */
874     public void commandSetRollerShutterStop(final String deviceId, ShutterAction.ShutterActions action) {
875         final DeviceStructureManager deviceStructMan = this.deviceStructMan;
876         if (deviceStructMan == null) {
877             return;
878         }
879         try {
880             final String capabilityId = deviceStructMan.getCapabilityId(deviceId,
881                     Capability.TYPE_ROLLERSHUTTERACTUATOR);
882             if (capabilityId == null) {
883                 return;
884             }
885             client.setRollerShutterAction(capabilityId, action);
886         } catch (IOException | ApiException | AuthenticationException e) {
887             handleClientException(e);
888         }
889     }
890
891     ScheduledExecutorService getScheduler() {
892         return scheduler;
893     }
894
895     InnogyClient createInnogyClient(final OAuthClientService oAuthService, final HttpClient httpClient) {
896         return new InnogyClient(oAuthService, httpClient);
897     }
898
899     /**
900      * Handles all Exceptions of the client communication. For minor "errors" like an already existing session, it
901      * returns true to inform the binding to continue running. In other cases it may e.g. schedule a reinitialization of
902      * the binding.
903      *
904      * @param e the Exception
905      * @return boolean true, if binding should continue.
906      */
907     private boolean handleClientException(final Exception e) {
908         boolean isReinitialize = true;
909         if (e instanceof SessionExistsException) {
910             logger.debug("Session already exists. Continuing...");
911             isReinitialize = false;
912         } else if (e instanceof InvalidActionTriggeredException) {
913             logger.debug("Error triggering action: {}", e.getMessage());
914             isReinitialize = false;
915         } else if (e instanceof RemoteAccessNotAllowedException) {
916             // Remote access not allowed (usually by IP address change)
917             logger.debug("Remote access not allowed. Dropping access token and reinitializing binding...");
918             refreshAccessToken();
919         } else if (e instanceof ControllerOfflineException) {
920             logger.debug("innogy SmartHome Controller is offline.");
921             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, e.getMessage());
922         } else if (e instanceof AuthenticationException) {
923             logger.debug("OAuthenticaton error, refreshing tokens: {}", e.getMessage());
924             refreshAccessToken();
925             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
926         } else if (e instanceof IOException) {
927             logger.debug("IO error: {}", e.getMessage());
928             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
929         } else if (e instanceof ApiException) {
930             logger.warn("Unexpected API error: {}", e.getMessage());
931             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
932         } else if (e instanceof TimeoutException) {
933             logger.debug("WebSocket timeout: {}", e.getMessage());
934         } else if (e instanceof SocketTimeoutException) {
935             logger.debug("Socket timeout: {}", e.getMessage());
936             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
937         } else if (e instanceof InterruptedException) {
938             isReinitialize = false;
939             Thread.currentThread().interrupt();
940         } else if (e instanceof ExecutionException) {
941             logger.debug("ExecutionException: {}", ExceptionUtils.getRootCauseMessage(e));
942             updateStatus(ThingStatus.OFFLINE);
943         } else {
944             logger.debug("Unknown exception", e);
945             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
946         }
947         if (isReinitialize) {
948             scheduleRestartClient(true);
949             return true;
950         }
951         return false;
952     }
953
954     private void refreshAccessToken() {
955         try {
956             final OAuthClientService localOAuthService = this.oAuthService;
957
958             if (localOAuthService != null) {
959                 oAuthService.refreshToken();
960             }
961         } catch (IOException | OAuthResponseException | OAuthException e) {
962             logger.debug("Could not refresh tokens", e);
963         }
964     }
965
966     /**
967      * Checks if the job is already (re-)scheduled.
968      * 
969      * @param job job to check
970      * @return true, when the job is already (re-)scheduled, otherwise false
971      */
972     private static boolean isAlreadyScheduled(ScheduledFuture<?> job) {
973         return job.getDelay(TimeUnit.SECONDS) > 0;
974     }
975 }