]> git.basschouten.com Git - openhab-addons.git/blob
5de2f8135c5489d621f2514fa4e65d5621833b6e
[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 @Nullable 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         OAuthClientService oAuthService = oAuthFactory.createOAuthClientService(thing.getUID().getAsString(), tokenURL,
157                 tokenURL, "clientId", "clientPass", null, true);
158         this.oAuthService = oAuthService;
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         OAuthClientService oAuthService = this.oAuthService;
354         if (oAuthService != null) {
355             oAuthService.removeAccessTokenRefreshListener(this);
356             oAuthFactory.ungetOAuthService(thing.getUID().getAsString());
357             this.oAuthService = null;
358         }
359         client = null;
360         deviceStructMan = null;
361
362         super.dispose();
363         logger.debug("LIVISI SmartHome bridge handler shut down.");
364     }
365
366     @Override
367     public void handleRemoval() {
368         OAuthClientService oAuthService = this.oAuthService;
369         if (oAuthService != null) {
370             oAuthFactory.deleteServiceAndAccessToken(thing.getUID().getAsString());
371         }
372         super.handleRemoval();
373     }
374
375     private synchronized void cancelJobs() {
376         if (cancelJob(reInitJob)) {
377             reInitJob = null;
378         }
379         if (cancelJob(bridgeRefreshJob)) {
380             bridgeRefreshJob = null;
381         }
382     }
383
384     private static boolean cancelJob(@Nullable ScheduledFuture<?> job) {
385         if (job != null) {
386             job.cancel(true);
387             return true;
388         }
389         return false;
390     }
391
392     /**
393      * Registers a {@link DeviceStatusListener}.
394      *
395      * @param deviceStatusListener listener
396      */
397     public void registerDeviceStatusListener(final String deviceId, final DeviceStatusListener deviceStatusListener) {
398         deviceStatusListeners.putIfAbsent(deviceId, deviceStatusListener);
399     }
400
401     /**
402      * Unregisters a {@link DeviceStatusListener}.
403      *
404      * @param deviceId id of the device to which the listener is registered
405      */
406     public void unregisterDeviceStatusListener(@Nullable final String deviceId) {
407         if (deviceId != null) {
408             deviceStatusListeners.remove(deviceId);
409         }
410     }
411
412     /**
413      * Loads a Collection of {@link DeviceDTO}s from the bridge and returns them.
414      *
415      * @return a Collection of {@link DeviceDTO}s
416      */
417     public Collection<DeviceDTO> loadDevices() {
418         return deviceStructMan.getDeviceList();
419     }
420
421     public boolean isSHCClassic() {
422         return getBridgeDevice().filter(DeviceDTO::isClassicController).isPresent();
423     }
424
425     /**
426      * Returns the bridge {@link DeviceDTO}.
427      *
428      * @return bridge {@link DeviceDTO}
429      */
430     private Optional<DeviceDTO> getBridgeDevice() {
431         return deviceStructMan.getBridgeDevice();
432     }
433
434     /**
435      * Returns the {@link DeviceDTO} with the given deviceId.
436      *
437      * @param deviceId device id
438      * @return {@link DeviceDTO} or null, if it does not exist or no {@link DeviceStructureManager} is available
439      */
440     public Optional<DeviceDTO> getDeviceById(final String deviceId) {
441         return deviceStructMan.getDeviceById(deviceId);
442     }
443
444     private void refreshBridgeState() {
445         Optional<DeviceDTO> bridgeOptional = getBridgeDevice();
446         if (bridgeOptional.isPresent()) {
447             try {
448                 DeviceDTO bridgeDevice = bridgeOptional.get();
449
450                 DeviceStateDTO deviceState = new DeviceStateDTO();
451                 deviceState.setId(bridgeDevice.getId());
452                 deviceState.setState(client.getDeviceStateByDeviceId(bridgeDevice.getId(), isSHCClassic()));
453                 bridgeDevice.setDeviceState(deviceState);
454             } catch (IOException e) {
455                 logger.debug("Exception occurred on reloading bridge", e);
456             }
457         }
458     }
459
460     /**
461      * Refreshes the {@link DeviceDTO} with the given id, by reloading the full device from the LIVISI webservice.
462      *
463      * @param deviceId device id
464      * @return the {@link DeviceDTO} or null, if it does not exist or no {@link DeviceStructureManager} is available
465      */
466     public Optional<DeviceDTO> refreshDevice(final String deviceId) {
467         try {
468             return deviceStructMan.refreshDevice(deviceId, isSHCClassic());
469         } catch (IOException e) {
470             handleClientException(e);
471         }
472         return Optional.empty();
473     }
474
475     @Override
476     public void onDeviceStateChanged(final DeviceDTO bridgeDevice) {
477         synchronized (this.lock) {
478             // DEVICE STATES
479             if (bridgeDevice.hasDeviceState()) {
480                 final boolean isSHCClassic = bridgeDevice.isClassicController();
481                 final Double cpuUsage = bridgeDevice.getDeviceState().getState().getCpuUsage(isSHCClassic).getValue();
482                 if (cpuUsage != null) {
483                     logger.debug("-> CPU usage state: {}", cpuUsage);
484                     updateState(CHANNEL_CPU, QuantityType.valueOf(cpuUsage, Units.PERCENT));
485                 }
486                 final Double diskUsage = bridgeDevice.getDeviceState().getState().getDiskUsage().getValue();
487                 if (diskUsage != null) {
488                     logger.debug("-> Disk usage state: {}", diskUsage);
489                     updateState(CHANNEL_DISK, QuantityType.valueOf(diskUsage, Units.PERCENT));
490                 }
491                 final Double memoryUsage = bridgeDevice.getDeviceState().getState().getMemoryUsage(isSHCClassic)
492                         .getValue();
493                 if (memoryUsage != null) {
494                     logger.debug("-> Memory usage state: {}", memoryUsage);
495                     updateState(CHANNEL_MEMORY, QuantityType.valueOf(memoryUsage, Units.PERCENT));
496                 }
497                 String operationStatus = bridgeDevice.getDeviceState().getState().getOperationStatus(isSHCClassic)
498                         .getValue();
499                 if (operationStatus != null) {
500                     logger.debug("-> Operation status: {}", operationStatus);
501                     updateState(CHANNEL_OPERATION_STATUS, new StringType(operationStatus.toUpperCase()));
502                 }
503             }
504         }
505     }
506
507     @Override
508     public void onDeviceStateChanged(final DeviceDTO bridgeDevice, final EventDTO event) {
509         synchronized (this.lock) {
510             if (event.isLinkedtoDevice()) {
511                 final boolean isSHCClassic = bridgeDevice.isClassicController();
512                 bridgeDevice.getDeviceState().getState().getOperationStatus(isSHCClassic)
513                         .setValue(event.getProperties().getOperationStatus(isSHCClassic));
514                 bridgeDevice.getDeviceState().getState().getCpuUsage(isSHCClassic)
515                         .setValue(event.getProperties().getCpuUsage(isSHCClassic));
516                 bridgeDevice.getDeviceState().getState().getDiskUsage().setValue(event.getProperties().getDiskUsage());
517                 bridgeDevice.getDeviceState().getState().getMemoryUsage(isSHCClassic)
518                         .setValue(event.getProperties().getMemoryUsage(isSHCClassic));
519                 onDeviceStateChanged(bridgeDevice);
520             }
521         }
522     }
523
524     @Override
525     public void onEvent(final String msg) {
526         logger.trace("onEvent called. Msg: {}", msg);
527
528         try {
529             final Optional<EventDTO> eventOptional = parseEvent(msg);
530             if (eventOptional.isPresent()) {
531                 EventDTO event = eventOptional.get();
532                 switch (event.getType()) {
533                     case BaseEventDTO.TYPE_STATE_CHANGED:
534                     case BaseEventDTO.TYPE_BUTTON_PRESSED:
535                         handleStateChangedEvent(event);
536                         break;
537                     case BaseEventDTO.TYPE_DISCONNECT:
538                         logger.debug("Websocket disconnected.");
539                         scheduleRestartClient(true);
540                         break;
541                     case BaseEventDTO.TYPE_CONFIGURATION_CHANGED:
542                         handleConfigurationChangedEvent(event);
543                         break;
544                     case BaseEventDTO.TYPE_CONTROLLER_CONNECTIVITY_CHANGED:
545                         handleControllerConnectivityChangedEvent(event);
546                         break;
547                     case BaseEventDTO.TYPE_NEW_MESSAGE_RECEIVED:
548                     case BaseEventDTO.TYPE_MESSAGE_CREATED:
549                         final Optional<MessageEventDTO> messageEvent = gson.fromJson(msg, MessageEventDTO.class);
550                         if (messageEvent.isPresent()) {
551                             handleNewMessageReceivedEvent(Objects.requireNonNull(messageEvent.get()));
552                         }
553                         break;
554                     case BaseEventDTO.TYPE_MESSAGE_DELETED:
555                         handleMessageDeletedEvent(event);
556                         break;
557                     default:
558                         logger.debug("Unsupported event type {}.", event.getType());
559                         break;
560                 }
561             }
562         } catch (IOException | 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 event
580      */
581     private void handleStateChangedEvent(final EventDTO event) throws IOException {
582         // CAPABILITY
583         if (event.isLinkedtoCapability()) {
584             logger.trace("Event is linked to capability");
585             final Optional<DeviceDTO> device = deviceStructMan.getDeviceByCapabilityId(event.getSourceId());
586             notifyDeviceStatusListeners(device, event);
587
588             // DEVICE
589         } else if (event.isLinkedtoDevice()) {
590             logger.trace("Event is linked to device");
591             final String sourceId = event.getSourceId();
592
593             final Optional<DeviceDTO> bridgeDevice = deviceStructMan.getBridgeDevice();
594             final Optional<DeviceDTO> device;
595             if (bridgeDevice.isPresent() && !sourceId.equals(bridgeDevice.get().getId())) {
596                 device = deviceStructMan.refreshDevice(sourceId, isSHCClassic());
597             } else {
598                 device = deviceStructMan.getDeviceById(sourceId);
599             }
600             notifyDeviceStatusListeners(device, event);
601
602         } else {
603             logger.debug("link type {} not supported (yet?)", event.getSourceLinkType());
604         }
605     }
606
607     /**
608      * Handles the event that occurs, when the connectivity of the bridge has changed.
609      *
610      * @param event event
611      */
612     private void handleControllerConnectivityChangedEvent(final EventDTO event) throws IOException {
613         final Boolean connected = event.getIsConnected();
614         if (connected != null) {
615             final ThingStatus thingStatus;
616             if (connected) {
617                 deviceStructMan.refreshDevices();
618                 thingStatus = ThingStatus.ONLINE;
619                 updateStatus(thingStatus);
620             } else {
621                 thingStatus = ThingStatus.OFFLINE;
622             }
623             logger.debug("SmartHome Controller connectivity changed to {} by {} event.", thingStatus,
624                     BaseEventDTO.TYPE_CONTROLLER_CONNECTIVITY_CHANGED);
625         } else {
626             logger.debug("isConnected property missing in {} event (returned null)!",
627                     BaseEventDTO.TYPE_CONTROLLER_CONNECTIVITY_CHANGED);
628         }
629     }
630
631     /**
632      * Handles the event that occurs, when a new message was received. Currently only handles low battery messages.
633      *
634      * @param event event
635      */
636     private void handleNewMessageReceivedEvent(final MessageEventDTO event) throws IOException {
637         final MessageDTO message = event.getMessage();
638         if (logger.isTraceEnabled()) {
639             logger.trace("Message: {}", gson.toJson(message));
640             logger.trace("Messagetype: {}", message.getType());
641         }
642         if (MessageDTO.TYPE_DEVICE_LOW_BATTERY.equals(message.getType()) && message.getDevices() != null) {
643             for (final String link : message.getDevices()) {
644                 final Optional<DeviceDTO> device = deviceStructMan.refreshDevice(LinkDTO.getId(link), isSHCClassic());
645                 notifyDeviceStatusListener(event.getSourceId(), device);
646             }
647         } else {
648             logger.debug("Message received event not yet implemented for Messagetype {}.", message.getType());
649         }
650     }
651
652     /**
653      * Handle the event that occurs, when a message was deleted. In case of a low battery message this means, that the
654      * device is back to normal. Currently, only messages linked to devices are handled by refreshing the device data
655      * and informing the {@link LivisiDeviceHandler} about the changed device.
656      *
657      * @param event event
658      */
659     private void handleMessageDeletedEvent(final EventDTO event) throws IOException {
660         final String messageId = event.getData().getId();
661         logger.debug("handleMessageDeletedEvent with messageId '{}'", messageId);
662
663         Optional<DeviceDTO> device = deviceStructMan.getDeviceWithMessageId(messageId);
664         if (device.isPresent()) {
665             String id = device.get().getId();
666             Optional<DeviceDTO> deviceRefreshed = deviceStructMan.refreshDevice(id, isSHCClassic());
667             notifyDeviceStatusListener(event.getSourceId(), deviceRefreshed);
668         } else {
669             logger.debug("No device found with message id {}.", messageId);
670         }
671     }
672
673     private void handleConfigurationChangedEvent(EventDTO event) {
674         if (configVersion.equals(event.getConfigurationVersion().toString())) {
675             logger.debug("Ignored configuration changed event with version '{}' as current version is '{}' the same.",
676                     event.getConfigurationVersion(), configVersion);
677         } else {
678             logger.info("Configuration changed from version {} to {}. Restarting LIVISI SmartHome binding...",
679                     configVersion, event.getConfigurationVersion());
680             scheduleRestartClient(false);
681         }
682     }
683
684     private void notifyDeviceStatusListener(String deviceId, Optional<DeviceDTO> device) {
685         if (device.isPresent()) {
686             DeviceStatusListener deviceStatusListener = deviceStatusListeners.get(device.get().getId());
687             if (deviceStatusListener != null) {
688                 deviceStatusListener.onDeviceStateChanged(device.get());
689             } else {
690                 logger.debug("No device status listener registered for device {}.", deviceId);
691             }
692         } else {
693             logger.debug("Unknown/unsupported device {}.", deviceId);
694         }
695     }
696
697     private void notifyDeviceStatusListeners(Optional<DeviceDTO> device, EventDTO event) {
698         String sourceId = event.getSourceId();
699         if (device.isPresent()) {
700             DeviceStatusListener deviceStatusListener = deviceStatusListeners.get(device.get().getId());
701             if (deviceStatusListener != null) {
702                 deviceStatusListener.onDeviceStateChanged(device.get(), event);
703             } else {
704                 logger.debug("No device status listener registered for device / capability {}.", sourceId);
705             }
706         } else {
707             logger.debug("Unknown/unsupported device / capability {}.", sourceId);
708         }
709     }
710
711     @Override
712     public void connectionClosed() {
713         scheduleRestartClient(true);
714     }
715
716     /**
717      * Sends the command to switch the {@link DeviceDTO} with the given id to the new state. Is called by the
718      * {@link LivisiDeviceHandler} for switch devices like the VariableActuator, PSS, PSSO or ISS2.
719      *
720      * @param deviceId device id
721      * @param state state (boolean)
722      */
723     public void commandSwitchDevice(final String deviceId, final boolean state) {
724         // VariableActuator
725         Optional<DeviceDTO> device = deviceStructMan.getDeviceById(deviceId);
726         if (device.isPresent()) {
727             final String deviceType = device.get().getType();
728             if (DEVICE_VARIABLE_ACTUATOR.equals(deviceType)) {
729                 executeCommand(deviceId, CapabilityDTO.TYPE_VARIABLEACTUATOR,
730                         (capabilityId) -> client.setVariableActuatorState(capabilityId, state));
731                 // PSS / PSSO / ISS2 / BT-PSS
732             } else if (DEVICE_PSS.equals(deviceType) || DEVICE_PSSO.equals(deviceType) || DEVICE_ISS2.equals(deviceType)
733                     || DEVICE_BT_PSS.equals((deviceType))) {
734                 executeCommand(deviceId, CapabilityDTO.TYPE_SWITCHACTUATOR,
735                         (capabilityId) -> client.setSwitchActuatorState(capabilityId, state));
736             }
737         } else {
738             logger.debug("No device with id {} could get found!", deviceId);
739         }
740     }
741
742     /**
743      * Sends the command to update the point temperature of the {@link DeviceDTO} with the given deviceId. Is called by
744      * the
745      * {@link LivisiDeviceHandler} for thermostat {@link DeviceDTO}s like RST or WRT.
746      *
747      * @param deviceId device id
748      * @param pointTemperature point temperature
749      */
750     public void commandUpdatePointTemperature(final String deviceId, final double pointTemperature) {
751         executeCommand(deviceId, CapabilityDTO.TYPE_THERMOSTATACTUATOR,
752                 (capabilityId) -> client.setPointTemperatureState(capabilityId, pointTemperature));
753     }
754
755     /**
756      * Sends the command to turn the alarm of the {@link DeviceDTO} with the given id on or off. Is called by the
757      * {@link LivisiDeviceHandler} for smoke detector {@link DeviceDTO}s like WSD or WSD2.
758      *
759      * @param deviceId device id
760      * @param alarmState alarm state (boolean)
761      */
762     public void commandSwitchAlarm(final String deviceId, final boolean alarmState) {
763         executeCommand(deviceId, CapabilityDTO.TYPE_ALARMACTUATOR,
764                 (capabilityId) -> client.setAlarmActuatorState(capabilityId, alarmState));
765     }
766
767     /**
768      * Sends the command to set the operation mode of the {@link DeviceDTO} with the given deviceId to auto (or manual,
769      * if
770      * false). Is called by the {@link LivisiDeviceHandler} for thermostat {@link DeviceDTO}s like RST.
771      *
772      * @param deviceId device id
773      * @param isAutoMode true activates the automatic mode, false the manual mode.
774      */
775     public void commandSetOperationMode(final String deviceId, final boolean isAutoMode) {
776         executeCommand(deviceId, CapabilityDTO.TYPE_THERMOSTATACTUATOR,
777                 (capabilityId) -> client.setOperationMode(capabilityId, isAutoMode));
778     }
779
780     /**
781      * Sends the command to set the dimm level of the {@link DeviceDTO} with the given id. Is called by the
782      * {@link LivisiDeviceHandler} for {@link DeviceDTO}s like ISD2 or PSD.
783      *
784      * @param deviceId device id
785      * @param dimLevel dim level
786      */
787     public void commandSetDimLevel(final String deviceId, final int dimLevel) {
788         executeCommand(deviceId, CapabilityDTO.TYPE_DIMMERACTUATOR,
789                 (capabilityId) -> client.setDimmerActuatorState(capabilityId, dimLevel));
790     }
791
792     /**
793      * Sends the command to set the rollershutter level of the {@link DeviceDTO} with the given id. Is called by the
794      * {@link LivisiDeviceHandler} for {@link DeviceDTO}s like ISR2.
795      *
796      * @param deviceId device id
797      * @param rollerShutterLevel roller shutter level
798      */
799     public void commandSetRollerShutterLevel(final String deviceId, final int rollerShutterLevel) {
800         executeCommand(deviceId, CapabilityDTO.TYPE_ROLLERSHUTTERACTUATOR,
801                 (capabilityId) -> client.setRollerShutterActuatorState(capabilityId, rollerShutterLevel));
802     }
803
804     /**
805      * Sends the command to start or stop moving the rollershutter (ISR2) in a specified direction
806      *
807      * @param deviceId device id
808      * @param action action
809      */
810     public void commandSetRollerShutterStop(final String deviceId, final ShutterActionType action) {
811         executeCommand(deviceId, CapabilityDTO.TYPE_ROLLERSHUTTERACTUATOR,
812                 (capabilityId) -> client.setRollerShutterAction(capabilityId, action));
813     }
814
815     private void executeCommand(final String deviceId, final String capabilityType,
816             final CommandExecutor commandExecutor) {
817         try {
818             final Optional<String> capabilityId = deviceStructMan.getCapabilityId(deviceId, capabilityType);
819             if (capabilityId.isPresent()) {
820                 commandExecutor.executeCommand(capabilityId.get());
821             }
822         } catch (IOException e) {
823             handleClientException(e);
824         }
825     }
826
827     ScheduledExecutorService getScheduler() {
828         return scheduler;
829     }
830
831     FullDeviceManager createFullDeviceManager(LivisiClient client) {
832         return new FullDeviceManager(client);
833     }
834
835     LivisiClient createClient(final OAuthClientService oAuthService) {
836         return new LivisiClient(bridgeConfiguration, oAuthService, new URLConnectionFactory());
837     }
838
839     /**
840      * Handles all Exceptions of the client communication. For minor "errors" like an already existing session, it
841      * returns true to inform the binding to continue running. In other cases it may e.g. schedule a reinitialization of
842      * the binding.
843      *
844      * @param e the Exception
845      * @return boolean true, if binding should continue.
846      */
847     private boolean handleClientException(final Exception e) {
848         boolean isReinitialize = true;
849         if (e instanceof SessionExistsException) {
850             logger.debug("Session already exists. Continuing...");
851             isReinitialize = false;
852         } else if (e instanceof InvalidActionTriggeredException) {
853             logger.debug("Error triggering action: {}", e.getMessage());
854             isReinitialize = false;
855         } else if (e instanceof RemoteAccessNotAllowedException) {
856             // Remote access not allowed (usually by IP address change)
857             logger.debug("Remote access not allowed. Dropping access token and reinitializing binding...");
858             refreshAccessToken();
859         } else if (e instanceof ControllerOfflineException) {
860             logger.debug("LIVISI SmartHome Controller is offline.");
861             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
862         } else if (e instanceof AuthenticationException) {
863             logger.debug("OAuthenticaton error, refreshing tokens: {}", e.getMessage());
864             refreshAccessToken();
865             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
866         } else if (e instanceof ApiException) {
867             logger.warn("Unexpected API error: {}", e.getMessage());
868             logger.debug("Unexpected API error", e);
869             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
870         } else if (e instanceof TimeoutException) {
871             logger.debug("WebSocket timeout: {}", e.getMessage());
872         } else if (e instanceof SocketTimeoutException) {
873             logger.debug("Socket timeout: {}", e.getMessage());
874             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
875         } else if (e instanceof IOException) {
876             logger.debug("IOException occurred", e);
877             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
878         } else if (e instanceof InterruptedException) {
879             isReinitialize = false;
880             Thread.currentThread().interrupt();
881         } else if (e instanceof ExecutionException) {
882             logger.debug("ExecutionException occurred", e);
883             updateStatus(ThingStatus.OFFLINE);
884         } else {
885             logger.debug("Unknown exception", e);
886             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
887         }
888         if (isReinitialize) {
889             scheduleRestartClient(true);
890         }
891         return isReinitialize;
892     }
893
894     private void refreshAccessToken() {
895         try {
896             requestAccessToken();
897         } catch (IOException | OAuthException | OAuthResponseException e) {
898             logger.debug("Could not refresh tokens", e);
899         }
900     }
901
902     private void requestAccessToken() throws OAuthException, IOException, OAuthResponseException {
903         OAuthClientService oAuthService = this.oAuthService;
904         if (oAuthService == null) {
905             throw new OAuthException("OAuth service is not initialized");
906         }
907         oAuthService.getAccessTokenByResourceOwnerPasswordCredentials(LivisiBindingConstants.USERNAME,
908                 bridgeConfiguration.password, null);
909     }
910
911     private Optional<EventDTO> parseEvent(final String msg) {
912         final Optional<BaseEventDTO> baseEventOptional = gson.fromJson(msg, BaseEventDTO.class);
913         if (baseEventOptional.isPresent()) {
914             BaseEventDTO baseEvent = baseEventOptional.get();
915             logger.debug("Event no {} found. Type: {}", baseEvent.getSequenceNumber(), baseEvent.getType());
916             if (BaseEventDTO.SUPPORTED_EVENT_TYPES.contains(baseEvent.getType())) {
917                 return gson.fromJson(msg, EventDTO.class);
918             }
919             logger.debug("Event type {} not supported. Skipping...", baseEvent.getType());
920         }
921         return Optional.empty();
922     }
923
924     /**
925      * Checks if the job is already (re-)scheduled.
926      *
927      * @param job job to check
928      * @return true, when the job is already (re-)scheduled, otherwise false
929      */
930     private static boolean isAlreadyScheduled(ScheduledFuture<?> job) {
931         return job.getDelay(TimeUnit.SECONDS) > 0;
932     }
933
934     @FunctionalInterface
935     private interface CommandExecutor {
936
937         void executeCommand(String capabilityId) throws IOException;
938     }
939 }