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.innogysmarthome.internal.handler;
15 import static org.openhab.binding.innogysmarthome.internal.InnogyBindingConstants.*;
16 import static org.openhab.binding.innogysmarthome.internal.client.Constants.API_URL_TOKEN;
18 import java.io.IOException;
19 import java.net.SocketTimeoutException;
21 import java.time.format.DateTimeFormatter;
22 import java.time.format.FormatStyle;
23 import java.util.Collection;
24 import java.util.Collections;
26 import java.util.Objects;
28 import java.util.concurrent.CopyOnWriteArraySet;
29 import java.util.concurrent.ExecutionException;
30 import java.util.concurrent.ScheduledExecutorService;
31 import java.util.concurrent.ScheduledFuture;
32 import java.util.concurrent.TimeUnit;
33 import java.util.concurrent.TimeoutException;
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.eclipse.jetty.client.HttpClient;
38 import org.openhab.binding.innogysmarthome.internal.InnogyWebSocket;
39 import org.openhab.binding.innogysmarthome.internal.client.InnogyClient;
40 import org.openhab.binding.innogysmarthome.internal.client.entity.action.ShutterAction;
41 import org.openhab.binding.innogysmarthome.internal.client.entity.capability.Capability;
42 import org.openhab.binding.innogysmarthome.internal.client.entity.device.Device;
43 import org.openhab.binding.innogysmarthome.internal.client.entity.device.DeviceConfig;
44 import org.openhab.binding.innogysmarthome.internal.client.entity.event.BaseEvent;
45 import org.openhab.binding.innogysmarthome.internal.client.entity.event.Event;
46 import org.openhab.binding.innogysmarthome.internal.client.entity.event.MessageEvent;
47 import org.openhab.binding.innogysmarthome.internal.client.entity.link.Link;
48 import org.openhab.binding.innogysmarthome.internal.client.entity.message.Message;
49 import org.openhab.binding.innogysmarthome.internal.client.exception.ApiException;
50 import org.openhab.binding.innogysmarthome.internal.client.exception.AuthenticationException;
51 import org.openhab.binding.innogysmarthome.internal.client.exception.ControllerOfflineException;
52 import org.openhab.binding.innogysmarthome.internal.client.exception.InvalidActionTriggeredException;
53 import org.openhab.binding.innogysmarthome.internal.client.exception.RemoteAccessNotAllowedException;
54 import org.openhab.binding.innogysmarthome.internal.client.exception.SessionExistsException;
55 import org.openhab.binding.innogysmarthome.internal.discovery.InnogyDeviceDiscoveryService;
56 import org.openhab.binding.innogysmarthome.internal.listener.DeviceStatusListener;
57 import org.openhab.binding.innogysmarthome.internal.listener.EventListener;
58 import org.openhab.binding.innogysmarthome.internal.manager.DeviceStructureManager;
59 import org.openhab.binding.innogysmarthome.internal.manager.FullDeviceManager;
60 import org.openhab.binding.innogysmarthome.internal.util.ExceptionUtils;
61 import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
62 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
63 import org.openhab.core.auth.client.oauth2.OAuthClientService;
64 import org.openhab.core.auth.client.oauth2.OAuthException;
65 import org.openhab.core.auth.client.oauth2.OAuthFactory;
66 import org.openhab.core.auth.client.oauth2.OAuthResponseException;
67 import org.openhab.core.config.core.Configuration;
68 import org.openhab.core.library.types.DecimalType;
69 import org.openhab.core.thing.Bridge;
70 import org.openhab.core.thing.ChannelUID;
71 import org.openhab.core.thing.Thing;
72 import org.openhab.core.thing.ThingStatus;
73 import org.openhab.core.thing.ThingStatusDetail;
74 import org.openhab.core.thing.ThingTypeUID;
75 import org.openhab.core.thing.binding.BaseBridgeHandler;
76 import org.openhab.core.thing.binding.ThingHandlerService;
77 import org.openhab.core.types.Command;
78 import org.slf4j.Logger;
79 import org.slf4j.LoggerFactory;
81 import com.google.gson.Gson;
84 * The {@link InnogyBridgeHandler} is responsible for handling the innogy SmartHome controller including the connection
85 * to the innogy backend for all communications with the innogy {@link Device}s.
87 * It implements the {@link AccessTokenRefreshListener} to handle updates of the oauth2 tokens and the
88 * {@link EventListener} to handle {@link Event}s, that are received by the {@link InnogyWebSocket}.
90 * The {@link Device}s are organized by the {@link DeviceStructureManager}, which is also responsible for the connection
91 * to the innogy SmartHome webservice via the {@link InnogyClient}.
93 * @author Oliver Kuhl - Initial contribution
94 * @author Hilbrand Bouwkamp - Refactored to use openHAB http and oauth2 libraries
97 public class InnogyBridgeHandler extends BaseBridgeHandler
98 implements AccessTokenRefreshListener, EventListener, DeviceStatusListener {
100 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);
102 private final Logger logger = LoggerFactory.getLogger(InnogyBridgeHandler.class);
103 private final Gson gson = new Gson();
104 private final Object lock = new Object();
105 private final Set<DeviceStatusListener> deviceStatusListeners = new CopyOnWriteArraySet<>();
106 private final OAuthFactory oAuthFactory;
107 private final HttpClient httpClient;
109 private @Nullable InnogyClient client;
110 private @Nullable InnogyWebSocket webSocket;
111 private @Nullable DeviceStructureManager deviceStructMan;
112 private @Nullable String bridgeId;
113 private @Nullable ScheduledFuture<?> reinitJob;
114 private @NonNullByDefault({}) InnogyBridgeConfiguration bridgeConfiguration;
115 private @Nullable OAuthClientService oAuthService;
118 * Constructs a new {@link InnogyBridgeHandler}.
120 * @param bridge Bridge thing to be used by this handler
121 * @param oAuthFactory Factory class to get OAuth2 service
122 * @param httpClient httpclient instance
124 public InnogyBridgeHandler(final Bridge bridge, final OAuthFactory oAuthFactory, final HttpClient httpClient) {
126 this.oAuthFactory = oAuthFactory;
127 this.httpClient = httpClient;
131 public void handleCommand(final ChannelUID channelUID, final Command command) {
136 public Collection<Class<? extends ThingHandlerService>> getServices() {
137 return Collections.singleton(InnogyDeviceDiscoveryService.class);
141 public void initialize() {
142 logger.debug("Initializing innogy SmartHome BridgeHandler...");
144 "The innogy SmartHome binding is deprecated and discontinued and will be removed with the next release of OpenHAB! Please migrate to the newer LIVISI SmartHome binding (which uses a local API which requires no backend servers and no internet connection).");
146 final InnogyBridgeConfiguration bridgeConfiguration = getConfigAs(InnogyBridgeConfiguration.class);
147 if (checkConfig(bridgeConfiguration)) {
148 this.bridgeConfiguration = bridgeConfiguration;
149 getScheduler().execute(this::initializeClient);
154 * Checks bridge configuration. If configuration is valid returns true.
156 * @return true if the configuration if valid
158 private boolean checkConfig(final InnogyBridgeConfiguration bridgeConfiguration) {
159 if (BRAND_INNOGY_SMARTHOME.equals(bridgeConfiguration.brand)) {
162 logger.debug("Invalid brand '{}'. Make sure to select a brand in the SHC thing configuration!",
163 bridgeConfiguration.brand);
164 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid brand '"
165 + bridgeConfiguration.brand + "'. Make sure to select a brand in the SHC thing configuration!");
171 * Initializes the services and InnogyClient.
173 private void initializeClient() {
174 final OAuthClientService oAuthService = oAuthFactory.createOAuthClientService(thing.getUID().getAsString(),
175 API_URL_TOKEN, API_URL_TOKEN, bridgeConfiguration.clientId, bridgeConfiguration.clientSecret, null,
177 this.oAuthService = oAuthService;
179 if (checkOnAuthCode()) {
180 final InnogyClient localClient = createInnogyClient(oAuthService, httpClient);
181 client = localClient;
182 deviceStructMan = new DeviceStructureManager(createFullDeviceManager(localClient));
183 oAuthService.addAccessTokenRefreshListener(this);
184 registerDeviceStatusListener(InnogyBridgeHandler.this);
185 scheduleRestartClient(false);
190 * Fetches the OAuth2 tokens from innogy SmartHome service if the auth code is set in the configuration and if
191 * successful removes the auth code. Returns true if the auth code was not set or if the authcode was successfully
192 * used to get a new refresh and access token.
194 * @return true if success
196 private boolean checkOnAuthCode() {
197 if (!bridgeConfiguration.authcode.isBlank()) {
198 logger.debug("Trying to get access and refresh tokens");
200 oAuthService.getAccessTokenResponseByAuthorizationCode(bridgeConfiguration.authcode,
201 bridgeConfiguration.redirectUrl);
202 final Configuration configuration = editConfiguration();
203 configuration.put(CONFIG_AUTH_CODE, "");
204 updateConfiguration(configuration);
205 } catch (IOException | OAuthException | OAuthResponseException e) {
206 logger.debug("Error fetching access tokens. Invalid authcode! Please generate a new one. Detail: {}",
208 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
209 "Cannot connect to innogy SmartHome service. Please set auth-code!");
217 * Initializes the client and connects to the innogy SmartHome service via Client API. Based on the provided
218 * {@Link Configuration} while constructing {@Link InnogyClient}, the given oauth2 access and refresh tokens are
219 * used or - if not yet available - new tokens are fetched from the service using the provided auth code.
221 private void startClient() {
223 logger.debug("Initializing innogy SmartHome client...");
224 final InnogyClient localClient = this.client;
225 if (localClient != null) {
226 localClient.refreshStatus();
228 } catch (AuthenticationException | ApiException | IOException e) {
229 if (handleClientException(e)) {
230 // If exception could not be handled properly it's no use to continue so we won't continue start
231 logger.debug("Error initializing innogy SmartHome client.", e);
235 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
236 if (deviceStructMan == null) {
240 deviceStructMan.refreshDevices();
241 } catch (IOException | ApiException | AuthenticationException e) {
242 if (handleClientException(e)) {
243 // If exception could not be handled properly it's no use to continue so we won't continue start
244 logger.debug("Error starting device structure manager.", e);
249 Device bridgeDevice = deviceStructMan.getBridgeDevice();
250 if (bridgeDevice == null) {
251 logger.debug("Failed to get bridge device, re-scheduling startClient.");
252 scheduleRestartClient(true);
255 setBridgeProperties(bridgeDevice);
256 bridgeId = bridgeDevice.getId();
261 * Start the websocket connection for receiving permanent update {@link Event}s from the innogy API.
263 private void startWebsocket() {
265 InnogyWebSocket localWebSocket = createWebSocket();
267 if (this.webSocket != null && this.webSocket.isRunning()) {
268 this.webSocket.stop();
269 this.webSocket = null;
272 logger.debug("Starting innogy websocket.");
273 this.webSocket = localWebSocket;
274 localWebSocket.start();
275 updateStatus(ThingStatus.ONLINE);
276 } catch (final Exception e) { // Catch Exception because websocket start throws Exception
277 logger.warn("Error starting websocket.", e);
278 handleClientException(e);
282 InnogyWebSocket createWebSocket() throws IOException, AuthenticationException {
283 final AccessTokenResponse accessTokenResponse = client.getAccessTokenResponse();
284 final String webSocketUrl = WEBSOCKET_API_URL_EVENTS.replace("{token}", accessTokenResponse.getAccessToken());
286 logger.debug("WebSocket URL: {}...{}", webSocketUrl.substring(0, 70),
287 webSocketUrl.substring(webSocketUrl.length() - 10));
289 return new InnogyWebSocket(this, URI.create(webSocketUrl), bridgeConfiguration.websocketidletimeout * 1000);
293 public void onAccessTokenResponse(final AccessTokenResponse credential) {
294 scheduleRestartClient(true);
298 * Schedules a re-initialization in the given future.
300 * @param delayed when it is scheduled delayed, it starts with a delay of
301 * {@link org.openhab.binding.innogysmarthome.internal.InnogyBindingConstants#REINITIALIZE_DELAY_SECONDS}
303 * otherwise it starts directly
305 private synchronized void scheduleRestartClient(final boolean delayed) {
307 final ScheduledFuture<?> localReinitJob = reinitJob;
309 if (localReinitJob != null && isAlreadyScheduled(localReinitJob)) {
310 logger.debug("Scheduling reinitialize - ignored: already triggered in {} seconds.",
311 localReinitJob.getDelay(TimeUnit.SECONDS));
315 final long seconds = delayed ? REINITIALIZE_DELAY_SECONDS : 0;
316 logger.debug("Scheduling reinitialize in {} seconds.", seconds);
317 reinitJob = getScheduler().schedule(this::startClient, seconds, TimeUnit.SECONDS);
320 private void setBridgeProperties(final Device bridgeDevice) {
321 final DeviceConfig config = bridgeDevice.getConfig();
323 logger.debug("Setting Bridge Device Properties for Bridge of type '{}' with ID '{}'", config.getName(),
324 bridgeDevice.getId());
325 final Map<String, String> properties = editProperties();
327 setPropertyIfPresent(Thing.PROPERTY_VENDOR, bridgeDevice.getManufacturer(), properties);
328 setPropertyIfPresent(Thing.PROPERTY_SERIAL_NUMBER, bridgeDevice.getSerialnumber(), properties);
329 setPropertyIfPresent(PROPERTY_ID, bridgeDevice.getId(), properties);
330 setPropertyIfPresent(Thing.PROPERTY_FIRMWARE_VERSION, config.getFirmwareVersion(), properties);
331 setPropertyIfPresent(Thing.PROPERTY_HARDWARE_VERSION, config.getHardwareVersion(), properties);
332 setPropertyIfPresent(PROPERTY_SOFTWARE_VERSION, config.getSoftwareVersion(), properties);
333 setPropertyIfPresent(PROPERTY_IP_ADDRESS, config.getIPAddress(), properties);
334 setPropertyIfPresent(Thing.PROPERTY_MAC_ADDRESS, config.getMACAddress(), properties);
335 if (config.getRegistrationTime() != null) {
336 properties.put(PROPERTY_REGISTRATION_TIME,
337 config.getRegistrationTime().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)));
339 setPropertyIfPresent(PROPERTY_CONFIGURATION_STATE, config.getConfigurationState(), properties);
340 setPropertyIfPresent(PROPERTY_SHC_TYPE, bridgeDevice.getType(), properties);
341 setPropertyIfPresent(PROPERTY_TIME_ZONE, config.getTimeZone(), properties);
342 setPropertyIfPresent(PROPERTY_PROTOCOL_ID, config.getProtocolId(), properties);
343 setPropertyIfPresent(PROPERTY_GEOLOCATION, config.getGeoLocation(), properties);
344 setPropertyIfPresent(PROPERTY_CURRENT_UTC_OFFSET, config.getCurrentUTCOffset(), properties);
345 setPropertyIfPresent(PROPERTY_BACKEND_CONNECTION_MONITORED, config.getBackendConnectionMonitored(), properties);
346 setPropertyIfPresent(PROPERTY_RFCOM_FAILURE_NOTIFICATION, config.getRFCommFailureNotification(), properties);
347 updateProperties(properties);
350 private void setPropertyIfPresent(final String key, final @Nullable Object data,
351 final Map<String, String> properties) {
353 properties.put(key, data instanceof String ? (String) data : data.toString());
358 public void dispose() {
359 logger.debug("Disposing innogy SmartHome bridge handler '{}'", getThing().getUID().getId());
360 unregisterDeviceStatusListener(this);
362 if (webSocket != null) {
367 deviceStructMan = null;
370 logger.debug("innogy SmartHome bridge handler shut down.");
373 private synchronized void cancelReinitJob() {
374 ScheduledFuture<?> reinitJob = this.reinitJob;
376 if (reinitJob != null) {
377 reinitJob.cancel(true);
378 this.reinitJob = null;
383 * Registers a {@link DeviceStatusListener}.
385 * @param deviceStatusListener
386 * @return true, if successful
388 public boolean registerDeviceStatusListener(final DeviceStatusListener deviceStatusListener) {
389 return deviceStatusListeners.add(deviceStatusListener);
393 * Unregisters a {@link DeviceStatusListener}.
395 * @param deviceStatusListener
396 * @return true, if successful
398 public boolean unregisterDeviceStatusListener(final DeviceStatusListener deviceStatusListener) {
399 return deviceStatusListeners.remove(deviceStatusListener);
403 * Loads a Collection of {@link Device}s from the bridge and returns them.
405 * @return a Collection of {@link Device}s
407 public Collection<Device> loadDevices() {
408 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
409 final Collection<Device> devices;
411 if (deviceStructMan == null) {
412 devices = Collections.emptyList();
414 devices = deviceStructMan.getDeviceList();
420 * Returns the {@link Device} with the given deviceId.
423 * @return {@link Device} or null, if it does not exist or no {@link DeviceStructureManager} is available
425 public @Nullable Device getDeviceById(final String deviceId) {
426 if (deviceStructMan != null) {
427 return deviceStructMan.getDeviceById(deviceId);
433 * Refreshes the {@link Device} with the given id, by reloading the full device from the innogy webservice.
436 * @return the {@link Device} or null, if it does not exist or no {@link DeviceStructureManager} is available
438 public @Nullable Device refreshDevice(final String deviceId) {
439 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
440 if (deviceStructMan == null) {
444 Device device = null;
446 deviceStructMan.refreshDevice(deviceId);
447 device = deviceStructMan.getDeviceById(deviceId);
448 } catch (IOException | ApiException | AuthenticationException e) {
449 handleClientException(e);
455 public void onDeviceStateChanged(final Device device) {
456 synchronized (this.lock) {
457 if (!bridgeId.equals(device.getId())) {
458 logger.trace("DeviceId {} not relevant for this handler (responsible for id {})", device.getId(),
463 logger.debug("onDeviceStateChanged called with device {}/{}", device.getConfig().getName(), device.getId());
466 if (device.hasDeviceState()) {
467 final Double cpuUsage = device.getDeviceState().getState().getCpuUsage().getValue();
468 if (cpuUsage != null) {
469 logger.debug("-> CPU usage state: {}", cpuUsage);
470 updateState(CHANNEL_CPU, new DecimalType(cpuUsage));
472 final Double diskUsage = device.getDeviceState().getState().getDiskUsage().getValue();
473 if (diskUsage != null) {
474 logger.debug("-> Disk usage state: {}", diskUsage);
475 updateState(CHANNEL_DISK, new DecimalType(diskUsage));
477 final Double memoryUsage = device.getDeviceState().getState().getMemoryUsage().getValue();
478 if (memoryUsage != null) {
479 logger.debug("-> Memory usage state: {}", memoryUsage);
480 updateState(CHANNEL_MEMORY, new DecimalType(memoryUsage));
489 public void onDeviceStateChanged(final Device device, final Event event) {
490 synchronized (this.lock) {
491 if (!bridgeId.equals(device.getId())) {
492 logger.trace("DeviceId {} not relevant for this handler (responsible for id {})", device.getId(),
497 logger.trace("DeviceId {} relevant for this handler.", device.getId());
499 if (event.isLinkedtoDevice() && DEVICE_SHCA.equals(device.getType())) {
500 device.getDeviceState().getState().getCpuUsage().setValue(event.getProperties().getCpuUsage());
501 device.getDeviceState().getState().getDiskUsage().setValue(event.getProperties().getDiskUsage());
502 device.getDeviceState().getState().getMemoryUsage().setValue(event.getProperties().getMemoryUsage());
503 onDeviceStateChanged(device);
509 public void onEvent(final String msg) {
510 logger.trace("onEvent called. Msg: {}", msg);
513 final BaseEvent be = gson.fromJson(msg, BaseEvent.class);
514 logger.debug("Event no {} found. Type: {}", be.getSequenceNumber(), be.getType());
515 if (!BaseEvent.SUPPORTED_EVENT_TYPES.contains(be.getType())) {
516 logger.debug("Event type {} not supported. Skipping...", be.getType());
518 final Event event = gson.fromJson(msg, Event.class);
520 switch (event.getType()) {
521 case BaseEvent.TYPE_STATE_CHANGED:
522 case BaseEvent.TYPE_BUTTON_PRESSED:
523 handleStateChangedEvent(event);
526 case BaseEvent.TYPE_DISCONNECT:
527 logger.debug("Websocket disconnected.");
528 scheduleRestartClient(true);
531 case BaseEvent.TYPE_CONFIGURATION_CHANGED:
532 if (client.getConfigVersion().equals(event.getConfigurationVersion().toString())) {
534 "Ignored configuration changed event with version '{}' as current version is '{}' the same.",
535 event.getConfigurationVersion(), client.getConfigVersion());
537 logger.info("Configuration changed from version {} to {}. Restarting innogy binding...",
538 client.getConfigVersion(), event.getConfigurationVersion());
539 scheduleRestartClient(false);
543 case BaseEvent.TYPE_CONTROLLER_CONNECTIVITY_CHANGED:
544 handleControllerConnectivityChangedEvent(event);
547 case BaseEvent.TYPE_NEW_MESSAGE_RECEIVED:
548 case BaseEvent.TYPE_MESSAGE_CREATED:
549 final MessageEvent messageEvent = gson.fromJson(msg, MessageEvent.class);
550 handleNewMessageReceivedEvent(Objects.requireNonNull(messageEvent));
553 case BaseEvent.TYPE_MESSAGE_DELETED:
554 handleMessageDeletedEvent(event);
558 logger.debug("Unsupported eventtype {}.", event.getType());
562 } catch (IOException | ApiException | AuthenticationException | 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.
580 * @throws ApiException
581 * @throws IOException
582 * @throws AuthenticationException
584 public void handleStateChangedEvent(final Event event) throws ApiException, IOException, AuthenticationException {
585 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
586 if (deviceStructMan == null) {
591 if (event.isLinkedtoCapability()) {
592 logger.trace("Event is linked to capability");
593 final Device device = deviceStructMan.getDeviceByCapabilityId(event.getSourceId());
594 if (device != null) {
595 for (final DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
596 deviceStatusListener.onDeviceStateChanged(device, event);
599 logger.debug("Unknown/unsupported device for capability {}.", event.getSource());
603 } else if (event.isLinkedtoDevice()) {
604 logger.trace("Event is linked to device");
606 if (!event.getSourceId().equals(deviceStructMan.getBridgeDevice().getId())) {
607 deviceStructMan.refreshDevice(event.getSourceId());
609 final Device device = deviceStructMan.getDeviceById(event.getSourceId());
610 if (device != null) {
611 for (final DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
612 deviceStatusListener.onDeviceStateChanged(device, event);
615 logger.debug("Unknown/unsupported device {}.", event.getSourceId());
619 logger.debug("link type {} not supported (yet?)", event.getSourceLinkType());
624 * Handles the event that occurs, when the connectivity of the bridge has changed.
627 * @throws ApiException
628 * @throws IOException
629 * @throws AuthenticationException
631 public void handleControllerConnectivityChangedEvent(final Event event)
632 throws ApiException, IOException, AuthenticationException {
633 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
634 if (deviceStructMan == null) {
637 final Boolean connected = event.getIsConnected();
638 if (connected != null) {
639 logger.debug("SmartHome Controller connectivity changed to {}.", connected ? "online" : "offline");
641 deviceStructMan.refreshDevices();
642 updateStatus(ThingStatus.ONLINE);
644 updateStatus(ThingStatus.OFFLINE);
647 logger.warn("isConnected property missing in event! (returned null)");
652 * Handles the event that occurs, when a new message was received. Currently only handles low battery messages.
655 * @throws ApiException
656 * @throws IOException
657 * @throws AuthenticationException
659 public void handleNewMessageReceivedEvent(final MessageEvent event)
660 throws ApiException, IOException, AuthenticationException {
661 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
662 if (deviceStructMan == null) {
665 final Message message = event.getMessage();
666 if (logger.isTraceEnabled()) {
667 logger.trace("Message: {}", gson.toJson(message));
668 logger.trace("Messagetype: {}", message.getType());
670 if (Message.TYPE_DEVICE_LOW_BATTERY.equals(message.getType()) && message.getDevices() != null) {
671 for (final String link : message.getDevices()) {
672 deviceStructMan.refreshDevice(Link.getId(link));
673 final Device device = deviceStructMan.getDeviceById(Link.getId(link));
674 if (device != null) {
675 for (final DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
676 deviceStatusListener.onDeviceStateChanged(device);
679 logger.debug("Unknown/unsupported device {}.", event.getSourceId());
683 logger.debug("Message received event not yet implemented for Messagetype {}.", message.getType());
688 * Handle the event that occurs, when a message was deleted. In case of a low battery message this means, that the
689 * device is back to normal. Currently, only messages linked to devices are handled by refreshing the device data
690 * and informing the {@link InnogyDeviceHandler} about the changed device.
693 * @throws ApiException
694 * @throws IOException
695 * @throws AuthenticationException
697 public void handleMessageDeletedEvent(final Event event) throws ApiException, IOException, AuthenticationException {
698 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
699 if (deviceStructMan == null) {
702 final String messageId = event.getData().getId();
704 logger.debug("handleMessageDeletedEvent with messageId '{}'", messageId);
705 Device device = deviceStructMan.getDeviceWithMessageId(messageId);
707 if (device != null) {
708 String id = device.getId();
709 deviceStructMan.refreshDevice(id);
710 device = deviceStructMan.getDeviceById(id);
711 if (device != null) {
712 for (final DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
713 deviceStatusListener.onDeviceStateChanged(device);
716 logger.debug("No device with id {} found after refresh.", id);
719 logger.debug("No device found with message id {}.", messageId);
724 public void connectionClosed() {
725 scheduleRestartClient(true);
729 * Sends the command to switch the {@link Device} with the given id to the new state. Is called by the
730 * {@link InnogyDeviceHandler} for switch devices like the VariableActuator, PSS, PSSO or ISS2.
735 public void commandSwitchDevice(final String deviceId, final boolean state) {
736 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
737 if (deviceStructMan == null) {
742 final String deviceType = deviceStructMan.getDeviceById(deviceId).getType();
743 if (DEVICE_VARIABLE_ACTUATOR.equals(deviceType)) {
744 final String capabilityId = deviceStructMan.getCapabilityId(deviceId, Capability.TYPE_VARIABLEACTUATOR);
745 if (capabilityId == null) {
748 client.setVariableActuatorState(capabilityId, state);
751 } else if (DEVICE_PSS.equals(deviceType) || DEVICE_PSSO.equals(deviceType)
752 || DEVICE_ISS2.equals(deviceType)) {
753 final String capabilityId = deviceStructMan.getCapabilityId(deviceId, Capability.TYPE_SWITCHACTUATOR);
754 if (capabilityId == null) {
757 client.setSwitchActuatorState(capabilityId, state);
759 } catch (IOException | ApiException | AuthenticationException e) {
760 handleClientException(e);
765 * Sends the command to update the point temperature of the {@link Device} with the given deviceId. Is called by the
766 * {@link InnogyDeviceHandler} for thermostat {@link Device}s like RST or WRT.
769 * @param pointTemperature
771 public void commandUpdatePointTemperature(final String deviceId, final double pointTemperature) {
772 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
773 if (deviceStructMan == null) {
777 final String capabilityId = deviceStructMan.getCapabilityId(deviceId, Capability.TYPE_THERMOSTATACTUATOR);
778 if (capabilityId == null) {
781 client.setPointTemperatureState(capabilityId, pointTemperature);
782 } catch (IOException | ApiException | AuthenticationException e) {
783 handleClientException(e);
788 * Sends the command to turn the alarm of the {@link Device} with the given id on or off. Is called by the
789 * {@link InnogyDeviceHandler} for smoke detector {@link Device}s like WSD or WSD2.
794 public void commandSwitchAlarm(final String deviceId, final boolean alarmState) {
795 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
796 if (deviceStructMan == null) {
800 final String capabilityId = deviceStructMan.getCapabilityId(deviceId, Capability.TYPE_ALARMACTUATOR);
801 if (capabilityId == null) {
804 client.setAlarmActuatorState(capabilityId, alarmState);
805 } catch (IOException | ApiException | AuthenticationException e) {
806 handleClientException(e);
811 * Sends the command to set the operation mode of the {@link Device} with the given deviceId to auto (or manual, if
812 * false). Is called by the {@link InnogyDeviceHandler} for thermostat {@link Device}s like RST.
815 * @param autoMode true activates the automatic mode, false the manual mode.
817 public void commandSetOperationMode(final String deviceId, final boolean autoMode) {
818 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
819 if (deviceStructMan == null) {
823 final String capabilityId = deviceStructMan.getCapabilityId(deviceId, Capability.TYPE_THERMOSTATACTUATOR);
824 if (capabilityId == null) {
827 client.setOperationMode(capabilityId, autoMode);
828 } catch (IOException | ApiException | AuthenticationException e) {
829 handleClientException(e);
834 * Sends the command to set the dimm level of the {@link Device} with the given id. Is called by the
835 * {@link InnogyDeviceHandler} for {@link Device}s like ISD2 or PSD.
840 public void commandSetDimmLevel(final String deviceId, final int dimLevel) {
841 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
842 if (deviceStructMan == null) {
846 final String capabilityId = deviceStructMan.getCapabilityId(deviceId, Capability.TYPE_DIMMERACTUATOR);
847 if (capabilityId == null) {
850 client.setDimmerActuatorState(capabilityId, dimLevel);
851 } catch (IOException | ApiException | AuthenticationException e) {
852 handleClientException(e);
857 * Sends the command to set the rollershutter level of the {@link Device} with the given id. Is called by the
858 * {@link InnogyDeviceHandler} for {@link Device}s like ISR2.
861 * @param rollerSchutterLevel
863 public void commandSetRollerShutterLevel(final String deviceId, final int rollerSchutterLevel) {
864 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
865 if (deviceStructMan == null) {
869 final String capabilityId = deviceStructMan.getCapabilityId(deviceId,
870 Capability.TYPE_ROLLERSHUTTERACTUATOR);
871 if (capabilityId == null) {
874 client.setRollerShutterActuatorState(capabilityId, rollerSchutterLevel);
875 } catch (IOException | ApiException | AuthenticationException e) {
876 handleClientException(e);
881 * Sends the command to start or stop moving the rollershutter (ISR2) in a specified direction
886 public void commandSetRollerShutterStop(final String deviceId, ShutterAction.ShutterActions action) {
887 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
888 if (deviceStructMan == null) {
892 final String capabilityId = deviceStructMan.getCapabilityId(deviceId,
893 Capability.TYPE_ROLLERSHUTTERACTUATOR);
894 if (capabilityId == null) {
897 client.setRollerShutterAction(capabilityId, action);
898 } catch (IOException | ApiException | AuthenticationException e) {
899 handleClientException(e);
903 ScheduledExecutorService getScheduler() {
907 FullDeviceManager createFullDeviceManager(InnogyClient client) {
908 return new FullDeviceManager(client);
911 InnogyClient createInnogyClient(final OAuthClientService oAuthService, final HttpClient httpClient) {
912 return new InnogyClient(oAuthService, httpClient);
916 * Handles all Exceptions of the client communication. For minor "errors" like an already existing session, it
917 * returns true to inform the binding to continue running. In other cases it may e.g. schedule a reinitialization of
920 * @param e the Exception
921 * @return boolean true, if binding should continue.
923 private boolean handleClientException(final Exception e) {
924 boolean isReinitialize = true;
925 if (e instanceof SessionExistsException) {
926 logger.debug("Session already exists. Continuing...");
927 isReinitialize = false;
928 } else if (e instanceof InvalidActionTriggeredException) {
929 logger.debug("Error triggering action: {}", e.getMessage());
930 isReinitialize = false;
931 } else if (e instanceof RemoteAccessNotAllowedException) {
932 // Remote access not allowed (usually by IP address change)
933 logger.debug("Remote access not allowed. Dropping access token and reinitializing binding...");
934 refreshAccessToken();
935 } else if (e instanceof ControllerOfflineException) {
936 logger.debug("innogy SmartHome Controller is offline.");
937 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, e.getMessage());
938 } else if (e instanceof AuthenticationException) {
939 logger.debug("OAuthenticaton error, refreshing tokens: {}", e.getMessage());
940 refreshAccessToken();
941 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
942 } else if (e instanceof IOException) {
943 logger.debug("IO error: {}", e.getMessage());
944 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
945 } else if (e instanceof ApiException) {
946 logger.warn("Unexpected API error: {}", e.getMessage());
947 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
948 } else if (e instanceof TimeoutException) {
949 logger.debug("WebSocket timeout: {}", e.getMessage());
950 } else if (e instanceof SocketTimeoutException) {
951 logger.debug("Socket timeout: {}", e.getMessage());
952 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
953 } else if (e instanceof InterruptedException) {
954 isReinitialize = false;
955 Thread.currentThread().interrupt();
956 } else if (e instanceof ExecutionException) {
957 logger.debug("ExecutionException: {}", ExceptionUtils.getRootThrowable(e).getMessage());
958 updateStatus(ThingStatus.OFFLINE);
960 logger.debug("Unknown exception", e);
961 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
963 if (isReinitialize) {
964 scheduleRestartClient(true);
970 private void refreshAccessToken() {
972 final OAuthClientService localOAuthService = this.oAuthService;
974 if (localOAuthService != null) {
975 oAuthService.refreshToken();
977 } catch (IOException | OAuthResponseException | OAuthException e) {
978 logger.debug("Could not refresh tokens", e);
983 * Checks if the job is already (re-)scheduled.
985 * @param job job to check
986 * @return true, when the job is already (re-)scheduled, otherwise false
988 private static boolean isAlreadyScheduled(ScheduledFuture<?> job) {
989 return job.getDelay(TimeUnit.SECONDS) > 0;