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