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