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