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