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