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