2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.livisismarthome.internal.handler;
15 import static org.openhab.binding.livisismarthome.internal.LivisiBindingConstants.*;
17 import java.io.IOException;
18 import java.net.SocketTimeoutException;
20 import java.time.format.DateTimeFormatter;
21 import java.time.format.FormatStyle;
22 import java.util.Collection;
23 import java.util.Collections;
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;
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;
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.
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}.
91 * The {@link DeviceDTO}s are organized by the {@link DeviceStructureManager}, which is also responsible for the
93 * to the LIVISI SmartHome webservice via the {@link LivisiClient}.
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
100 public class LivisiBridgeHandler extends BaseBridgeHandler
101 implements AccessTokenRefreshListener, EventListener, DeviceStatusListener {
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;
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 = "";
121 * Constructs a new {@link LivisiBridgeHandler}.
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
127 public LivisiBridgeHandler(final Bridge bridge, final OAuthFactory oAuthFactory, final HttpClient httpClient) {
129 this.oAuthFactory = oAuthFactory;
130 this.httpClient = httpClient;
134 public void handleCommand(final ChannelUID channelUID, final Command command) {
139 public Collection<Class<? extends ThingHandlerService>> getServices() {
140 return Collections.singleton(LivisiDeviceDiscoveryService.class);
144 public void initialize() {
145 logger.debug("Initializing LIVISI SmartHome BridgeHandler...");
146 bridgeConfiguration = getConfigAs(LivisiBridgeConfiguration.class);
147 updateStatus(ThingStatus.UNKNOWN);
152 * Initializes the services and LivisiClient.
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);
163 getScheduler().schedule(() -> {
165 requestAccessToken();
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");
172 }, 0, TimeUnit.SECONDS);
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.
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);
190 registerDeviceStatusListener(bridgeDevice.getId(), this);
191 onDeviceStateChanged(bridgeDevice); // initialize channels
192 scheduleBridgeRefreshJob(bridgeDevice);
194 startWebSocket(bridgeDevice);
196 logger.debug("Failed to get bridge device, re-scheduling startClient.");
197 scheduleRestartClient(true);
202 private boolean refreshDevices() {
204 configVersion = client.refreshStatus();
205 deviceStructMan.refreshDevices();
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);
217 * Start the websocket connection for receiving permanent update {@link EventDTO}s from the LIVISI API.
219 private void startWebSocket(DeviceDTO bridgeDevice) {
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);
232 private void stopWebSocket() {
233 LivisiWebSocket webSocket = this.webSocket;
234 if (webSocket != null && webSocket.isRunning()) {
235 logger.debug("Stopping LIVISI SmartHome websocket.");
237 this.webSocket = null;
242 LivisiWebSocket createAndStartWebSocket(DeviceDTO bridgeDevice) throws IOException {
243 final Optional<String> accessToken = getAccessToken(client);
244 if (accessToken.isEmpty()) {
248 final String webSocketUrl = URLCreator.createEventsURL(bridgeConfiguration.host, accessToken.get(),
249 bridgeDevice.isClassicController());
251 logger.debug("WebSocket URL: {}...{}", webSocketUrl.substring(0, 70),
252 webSocketUrl.substring(webSocketUrl.length() - 10));
254 LivisiWebSocket webSocket = new LivisiWebSocket(httpClient, this, URI.create(webSocketUrl),
255 bridgeConfiguration.webSocketIdleTimeout * 1000);
260 private static Optional<String> getAccessToken(LivisiClient client) throws IOException {
261 return Optional.of(client.getAccessTokenResponse().getAccessToken());
265 public void onAccessTokenResponse(final AccessTokenResponse credential) {
266 scheduleRestartClient(true);
270 * Schedules a re-initialization in the given future.
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}
275 * otherwise it starts directly
277 private synchronized void scheduleRestartClient(final boolean delayed) {
278 final ScheduledFuture<?> reInitJobLocal = this.reInitJob;
279 if (reInitJobLocal == null || !isAlreadyScheduled(reInitJobLocal)) {
280 long delaySeconds = 0;
282 delaySeconds = REINITIALIZE_DELAY_SECONDS;
284 logger.debug("Scheduling reinitialize in {} delaySeconds.", delaySeconds);
285 this.reInitJob = getScheduler().schedule(this::startClient, delaySeconds, TimeUnit.SECONDS);
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.
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);
300 this.bridgeRefreshJob = getScheduler().scheduleWithFixedDelay(() -> {
301 logger.debug("Refreshing bridge");
303 refreshBridgeState();
304 onDeviceStateChanged(bridgeDevice);
305 }, BRIDGE_REFRESH_SECONDS, BRIDGE_REFRESH_SECONDS, TimeUnit.SECONDS);
310 private void setBridgeProperties(final DeviceDTO bridgeDevice) {
311 final DeviceConfigDTO config = bridgeDevice.getConfig();
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();
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)));
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);
340 private void setPropertyIfPresent(final String key, final @Nullable Object data,
341 final Map<String, String> properties) {
343 properties.put(key, data.toString());
348 public void dispose() {
349 logger.debug("Disposing LIVISI SmartHome bridge handler '{}'", getThing().getUID().getId());
350 unregisterDeviceStatusListener(bridgeId);
353 OAuthClientService oAuthService = this.oAuthService;
354 if (oAuthService != null) {
355 oAuthService.removeAccessTokenRefreshListener(this);
356 oAuthFactory.ungetOAuthService(thing.getUID().getAsString());
357 this.oAuthService = null;
360 deviceStructMan = null;
363 logger.debug("LIVISI SmartHome bridge handler shut down.");
367 public void handleRemoval() {
368 OAuthClientService oAuthService = this.oAuthService;
369 if (oAuthService != null) {
370 oAuthFactory.deleteServiceAndAccessToken(thing.getUID().getAsString());
372 super.handleRemoval();
375 private synchronized void cancelJobs() {
376 if (cancelJob(reInitJob)) {
379 if (cancelJob(bridgeRefreshJob)) {
380 bridgeRefreshJob = null;
384 private static boolean cancelJob(@Nullable ScheduledFuture<?> job) {
393 * Registers a {@link DeviceStatusListener}.
395 * @param deviceStatusListener listener
397 public void registerDeviceStatusListener(final String deviceId, final DeviceStatusListener deviceStatusListener) {
398 deviceStatusListeners.putIfAbsent(deviceId, deviceStatusListener);
402 * Unregisters a {@link DeviceStatusListener}.
404 * @param deviceId id of the device to which the listener is registered
406 public void unregisterDeviceStatusListener(@Nullable final String deviceId) {
407 if (deviceId != null) {
408 deviceStatusListeners.remove(deviceId);
413 * Loads a Collection of {@link DeviceDTO}s from the bridge and returns them.
415 * @return a Collection of {@link DeviceDTO}s
417 public Collection<DeviceDTO> loadDevices() {
418 return deviceStructMan.getDeviceList();
421 public boolean isSHCClassic() {
422 return getBridgeDevice().filter(DeviceDTO::isClassicController).isPresent();
426 * Returns the bridge {@link DeviceDTO}.
428 * @return bridge {@link DeviceDTO}
430 private Optional<DeviceDTO> getBridgeDevice() {
431 return deviceStructMan.getBridgeDevice();
435 * Returns the {@link DeviceDTO} with the given deviceId.
437 * @param deviceId device id
438 * @return {@link DeviceDTO} or null, if it does not exist or no {@link DeviceStructureManager} is available
440 public Optional<DeviceDTO> getDeviceById(final String deviceId) {
441 return deviceStructMan.getDeviceById(deviceId);
444 private void refreshBridgeState() {
445 Optional<DeviceDTO> bridgeOptional = getBridgeDevice();
446 if (bridgeOptional.isPresent()) {
448 DeviceDTO bridgeDevice = bridgeOptional.get();
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);
461 * Refreshes the {@link DeviceDTO} with the given id, by reloading the full device from the LIVISI webservice.
463 * @param deviceId device id
464 * @return the {@link DeviceDTO} or null, if it does not exist or no {@link DeviceStructureManager} is available
466 public Optional<DeviceDTO> refreshDevice(final String deviceId) {
468 return deviceStructMan.refreshDevice(deviceId, isSHCClassic());
469 } catch (IOException e) {
470 handleClientException(e);
472 return Optional.empty();
476 public void onDeviceStateChanged(final DeviceDTO bridgeDevice) {
477 synchronized (this.lock) {
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));
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));
491 final Double memoryUsage = bridgeDevice.getDeviceState().getState().getMemoryUsage(isSHCClassic)
493 if (memoryUsage != null) {
494 logger.debug("-> Memory usage state: {}", memoryUsage);
495 updateState(CHANNEL_MEMORY, QuantityType.valueOf(memoryUsage, Units.PERCENT));
497 String operationStatus = bridgeDevice.getDeviceState().getState().getOperationStatus(isSHCClassic)
499 if (operationStatus != null) {
500 logger.debug("-> Operation status: {}", operationStatus);
501 updateState(CHANNEL_OPERATION_STATUS, new StringType(operationStatus.toUpperCase()));
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);
525 public void onEvent(final String msg) {
526 logger.trace("onEvent called. Msg: {}", msg);
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);
537 case BaseEventDTO.TYPE_DISCONNECT:
538 logger.debug("Websocket disconnected.");
539 scheduleRestartClient(true);
541 case BaseEventDTO.TYPE_CONFIGURATION_CHANGED:
542 handleConfigurationChangedEvent(event);
544 case BaseEventDTO.TYPE_CONTROLLER_CONNECTIVITY_CHANGED:
545 handleControllerConnectivityChangedEvent(event);
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()));
554 case BaseEventDTO.TYPE_MESSAGE_DELETED:
555 handleMessageDeletedEvent(event);
558 logger.debug("Unsupported event type {}.", event.getType());
562 } catch (IOException | RuntimeException e) {
563 logger.debug("Error with Event: {}", e.getMessage(), e);
564 handleClientException(e);
569 public void onError(final Throwable cause) {
570 if (cause instanceof Exception) {
571 handleClientException((Exception) cause);
576 * Handles the event that occurs, when the state of a device (like reachability) or a capability (like a temperature
577 * value) has changed.
581 private void handleStateChangedEvent(final EventDTO event) throws IOException {
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);
589 } else if (event.isLinkedtoDevice()) {
590 logger.trace("Event is linked to device");
591 final String sourceId = event.getSourceId();
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());
598 device = deviceStructMan.getDeviceById(sourceId);
600 notifyDeviceStatusListeners(device, event);
603 logger.debug("link type {} not supported (yet?)", event.getSourceLinkType());
608 * Handles the event that occurs, when the connectivity of the bridge has changed.
612 private void handleControllerConnectivityChangedEvent(final EventDTO event) throws IOException {
613 final Boolean connected = event.getIsConnected();
614 if (connected != null) {
615 final ThingStatus thingStatus;
617 deviceStructMan.refreshDevices();
618 thingStatus = ThingStatus.ONLINE;
619 updateStatus(thingStatus);
621 thingStatus = ThingStatus.OFFLINE;
623 logger.debug("SmartHome Controller connectivity changed to {} by {} event.", thingStatus,
624 BaseEventDTO.TYPE_CONTROLLER_CONNECTIVITY_CHANGED);
626 logger.debug("isConnected property missing in {} event (returned null)!",
627 BaseEventDTO.TYPE_CONTROLLER_CONNECTIVITY_CHANGED);
632 * Handles the event that occurs, when a new message was received. Currently only handles low battery messages.
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());
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);
648 logger.debug("Message received event not yet implemented for Messagetype {}.", message.getType());
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.
659 private void handleMessageDeletedEvent(final EventDTO event) throws IOException {
660 final String messageId = event.getData().getId();
661 logger.debug("handleMessageDeletedEvent with messageId '{}'", messageId);
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);
669 logger.debug("No device found with message id {}.", messageId);
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);
678 logger.info("Configuration changed from version {} to {}. Restarting LIVISI SmartHome binding...",
679 configVersion, event.getConfigurationVersion());
680 scheduleRestartClient(false);
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());
690 logger.debug("No device status listener registered for device {}.", deviceId);
693 logger.debug("Unknown/unsupported device {}.", deviceId);
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);
704 logger.debug("No device status listener registered for device / capability {}.", sourceId);
707 logger.debug("Unknown/unsupported device / capability {}.", sourceId);
712 public void connectionClosed() {
713 scheduleRestartClient(true);
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.
720 * @param deviceId device id
721 * @param state state (boolean)
723 public void commandSwitchDevice(final String deviceId, final boolean state) {
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));
738 logger.debug("No device with id {} could get found!", deviceId);
743 * Sends the command to update the point temperature of the {@link DeviceDTO} with the given deviceId. Is called by
745 * {@link LivisiDeviceHandler} for thermostat {@link DeviceDTO}s like RST or WRT.
747 * @param deviceId device id
748 * @param pointTemperature point temperature
750 public void commandUpdatePointTemperature(final String deviceId, final double pointTemperature) {
751 executeCommand(deviceId, CapabilityDTO.TYPE_THERMOSTATACTUATOR,
752 (capabilityId) -> client.setPointTemperatureState(capabilityId, pointTemperature));
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.
759 * @param deviceId device id
760 * @param alarmState alarm state (boolean)
762 public void commandSwitchAlarm(final String deviceId, final boolean alarmState) {
763 executeCommand(deviceId, CapabilityDTO.TYPE_ALARMACTUATOR,
764 (capabilityId) -> client.setAlarmActuatorState(capabilityId, alarmState));
768 * Sends the command to set the operation mode of the {@link DeviceDTO} with the given deviceId to auto (or manual,
770 * false). Is called by the {@link LivisiDeviceHandler} for thermostat {@link DeviceDTO}s like RST.
772 * @param deviceId device id
773 * @param isAutoMode true activates the automatic mode, false the manual mode.
775 public void commandSetOperationMode(final String deviceId, final boolean isAutoMode) {
776 executeCommand(deviceId, CapabilityDTO.TYPE_THERMOSTATACTUATOR,
777 (capabilityId) -> client.setOperationMode(capabilityId, isAutoMode));
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.
784 * @param deviceId device id
785 * @param dimLevel dim level
787 public void commandSetDimLevel(final String deviceId, final int dimLevel) {
788 executeCommand(deviceId, CapabilityDTO.TYPE_DIMMERACTUATOR,
789 (capabilityId) -> client.setDimmerActuatorState(capabilityId, dimLevel));
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.
796 * @param deviceId device id
797 * @param rollerShutterLevel roller shutter level
799 public void commandSetRollerShutterLevel(final String deviceId, final int rollerShutterLevel) {
800 executeCommand(deviceId, CapabilityDTO.TYPE_ROLLERSHUTTERACTUATOR,
801 (capabilityId) -> client.setRollerShutterActuatorState(capabilityId, rollerShutterLevel));
805 * Sends the command to start or stop moving the rollershutter (ISR2) in a specified direction
807 * @param deviceId device id
808 * @param action action
810 public void commandSetRollerShutterStop(final String deviceId, final ShutterActionType action) {
811 executeCommand(deviceId, CapabilityDTO.TYPE_ROLLERSHUTTERACTUATOR,
812 (capabilityId) -> client.setRollerShutterAction(capabilityId, action));
815 private void executeCommand(final String deviceId, final String capabilityType,
816 final CommandExecutor commandExecutor) {
818 final Optional<String> capabilityId = deviceStructMan.getCapabilityId(deviceId, capabilityType);
819 if (capabilityId.isPresent()) {
820 commandExecutor.executeCommand(capabilityId.get());
822 } catch (IOException e) {
823 handleClientException(e);
827 ScheduledExecutorService getScheduler() {
831 FullDeviceManager createFullDeviceManager(LivisiClient client) {
832 return new FullDeviceManager(client);
835 LivisiClient createClient(final OAuthClientService oAuthService) {
836 return new LivisiClient(bridgeConfiguration, oAuthService, new URLConnectionFactory());
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
844 * @param e the Exception
845 * @return boolean true, if binding should continue.
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);
885 logger.debug("Unknown exception", e);
886 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
888 if (isReinitialize) {
889 scheduleRestartClient(true);
891 return isReinitialize;
894 private void refreshAccessToken() {
896 requestAccessToken();
897 } catch (IOException | OAuthException | OAuthResponseException e) {
898 logger.debug("Could not refresh tokens", e);
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");
907 oAuthService.getAccessTokenByResourceOwnerPasswordCredentials(LivisiBindingConstants.USERNAME,
908 bridgeConfiguration.password, null);
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);
919 logger.debug("Event type {} not supported. Skipping...", baseEvent.getType());
921 return Optional.empty();
925 * Checks if the job is already (re-)scheduled.
927 * @param job job to check
928 * @return true, when the job is already (re-)scheduled, otherwise false
930 private static boolean isAlreadyScheduled(ScheduledFuture<?> job) {
931 return job.getDelay(TimeUnit.SECONDS) > 0;
935 private interface CommandExecutor {
937 void executeCommand(String capabilityId) throws IOException;