2 * Copyright (c) 2010-2021 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;
24 import java.util.concurrent.*;
26 import org.apache.commons.lang.StringUtils;
27 import org.apache.commons.lang.exception.ExceptionUtils;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.client.HttpClient;
31 import org.openhab.binding.innogysmarthome.internal.InnogyWebSocket;
32 import org.openhab.binding.innogysmarthome.internal.client.InnogyClient;
33 import org.openhab.binding.innogysmarthome.internal.client.entity.action.ShutterAction;
34 import org.openhab.binding.innogysmarthome.internal.client.entity.capability.Capability;
35 import org.openhab.binding.innogysmarthome.internal.client.entity.device.Device;
36 import org.openhab.binding.innogysmarthome.internal.client.entity.device.DeviceConfig;
37 import org.openhab.binding.innogysmarthome.internal.client.entity.event.BaseEvent;
38 import org.openhab.binding.innogysmarthome.internal.client.entity.event.Event;
39 import org.openhab.binding.innogysmarthome.internal.client.entity.event.MessageEvent;
40 import org.openhab.binding.innogysmarthome.internal.client.entity.link.Link;
41 import org.openhab.binding.innogysmarthome.internal.client.entity.message.Message;
42 import org.openhab.binding.innogysmarthome.internal.client.exception.ApiException;
43 import org.openhab.binding.innogysmarthome.internal.client.exception.AuthenticationException;
44 import org.openhab.binding.innogysmarthome.internal.client.exception.ControllerOfflineException;
45 import org.openhab.binding.innogysmarthome.internal.client.exception.InvalidActionTriggeredException;
46 import org.openhab.binding.innogysmarthome.internal.client.exception.RemoteAccessNotAllowedException;
47 import org.openhab.binding.innogysmarthome.internal.client.exception.SessionExistsException;
48 import org.openhab.binding.innogysmarthome.internal.discovery.InnogyDeviceDiscoveryService;
49 import org.openhab.binding.innogysmarthome.internal.listener.DeviceStatusListener;
50 import org.openhab.binding.innogysmarthome.internal.listener.EventListener;
51 import org.openhab.binding.innogysmarthome.internal.manager.DeviceStructureManager;
52 import org.openhab.binding.innogysmarthome.internal.manager.FullDeviceManager;
53 import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
54 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
55 import org.openhab.core.auth.client.oauth2.OAuthClientService;
56 import org.openhab.core.auth.client.oauth2.OAuthException;
57 import org.openhab.core.auth.client.oauth2.OAuthFactory;
58 import org.openhab.core.auth.client.oauth2.OAuthResponseException;
59 import org.openhab.core.config.core.Configuration;
60 import org.openhab.core.library.types.DecimalType;
61 import org.openhab.core.thing.Bridge;
62 import org.openhab.core.thing.ChannelUID;
63 import org.openhab.core.thing.Thing;
64 import org.openhab.core.thing.ThingStatus;
65 import org.openhab.core.thing.ThingStatusDetail;
66 import org.openhab.core.thing.ThingTypeUID;
67 import org.openhab.core.thing.binding.BaseBridgeHandler;
68 import org.openhab.core.thing.binding.ThingHandlerService;
69 import org.openhab.core.types.Command;
70 import org.slf4j.Logger;
71 import org.slf4j.LoggerFactory;
73 import com.google.gson.Gson;
76 * The {@link InnogyBridgeHandler} is responsible for handling the innogy SmartHome controller including the connection
77 * to the innogy backend for all communications with the innogy {@link Device}s.
79 * It implements the {@link AccessTokenRefreshListener} to handle updates of the oauth2 tokens and the
80 * {@link EventListener} to handle {@link Event}s, that are received by the {@link InnogyWebSocket}.
82 * The {@link Device}s are organized by the {@link DeviceStructureManager}, which is also responsible for the connection
83 * to the innogy SmartHome webservice via the {@link InnogyClient}.
85 * @author Oliver Kuhl - Initial contribution
86 * @author Hilbrand Bouwkamp - Refactored to use openHAB http and oauth2 libraries
89 public class InnogyBridgeHandler extends BaseBridgeHandler
90 implements AccessTokenRefreshListener, EventListener, DeviceStatusListener {
92 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);
94 private final Logger logger = LoggerFactory.getLogger(InnogyBridgeHandler.class);
95 private final Gson gson = new Gson();
96 private final Object lock = new Object();
97 private final Set<DeviceStatusListener> deviceStatusListeners = new CopyOnWriteArraySet<>();
98 private final OAuthFactory oAuthFactory;
99 private final HttpClient httpClient;
101 private @Nullable InnogyClient client;
102 private @Nullable InnogyWebSocket webSocket;
103 private @Nullable DeviceStructureManager deviceStructMan;
104 private @Nullable String bridgeId;
105 private @Nullable ScheduledFuture<?> reinitJob;
106 private @NonNullByDefault({}) InnogyBridgeConfiguration bridgeConfiguration;
107 private @Nullable OAuthClientService oAuthService;
110 * Constructs a new {@link InnogyBridgeHandler}.
112 * @param bridge Bridge thing to be used by this handler
113 * @param oAuthFactory Factory class to get OAuth2 service
114 * @param httpClient httpclient instance
116 public InnogyBridgeHandler(final Bridge bridge, final OAuthFactory oAuthFactory, final HttpClient httpClient) {
118 this.oAuthFactory = oAuthFactory;
119 this.httpClient = httpClient;
123 public void handleCommand(final ChannelUID channelUID, final Command command) {
128 public Collection<Class<? extends ThingHandlerService>> getServices() {
129 return Collections.singleton(InnogyDeviceDiscoveryService.class);
133 public void initialize() {
134 logger.debug("Initializing innogy SmartHome BridgeHandler...");
135 final InnogyBridgeConfiguration bridgeConfiguration = getConfigAs(InnogyBridgeConfiguration.class);
136 if (checkConfig(bridgeConfiguration)) {
137 this.bridgeConfiguration = bridgeConfiguration;
138 getScheduler().execute(this::initializeClient);
143 * Checks bridge configuration. If configuration is valid returns true.
145 * @return true if the configuration if valid
147 private boolean checkConfig(final InnogyBridgeConfiguration bridgeConfiguration) {
148 if (BRAND_INNOGY_SMARTHOME.equals(bridgeConfiguration.brand)) {
151 logger.debug("Invalid brand '{}'. Make sure to select a brand in the SHC thing configuration!",
152 bridgeConfiguration.brand);
153 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid brand '"
154 + bridgeConfiguration.brand + "'. Make sure to select a brand in the SHC thing configuration!");
160 * Initializes the services and InnogyClient.
162 private void initializeClient() {
163 final OAuthClientService oAuthService = oAuthFactory.createOAuthClientService(thing.getUID().getAsString(),
164 API_URL_TOKEN, API_URL_TOKEN, bridgeConfiguration.clientId, bridgeConfiguration.clientSecret, null,
166 this.oAuthService = oAuthService;
168 if (checkOnAuthCode()) {
169 final InnogyClient localClient = createInnogyClient(oAuthService, httpClient);
170 client = localClient;
171 deviceStructMan = new DeviceStructureManager(createFullDeviceManager(localClient));
172 oAuthService.addAccessTokenRefreshListener(this);
173 registerDeviceStatusListener(InnogyBridgeHandler.this);
174 scheduleRestartClient(false);
179 * Fetches the OAuth2 tokens from innogy SmartHome service if the auth code is set in the configuration and if
180 * successful removes the auth code. Returns true if the auth code was not set or if the authcode was successfully
181 * used to get a new refresh and access token.
183 * @return true if success
185 private boolean checkOnAuthCode() {
186 if (StringUtils.isNotBlank(bridgeConfiguration.authcode)) {
187 logger.debug("Trying to get access and refresh tokens");
189 oAuthService.getAccessTokenResponseByAuthorizationCode(bridgeConfiguration.authcode,
190 bridgeConfiguration.redirectUrl);
191 final Configuration configuration = editConfiguration();
192 configuration.put(CONFIG_AUTH_CODE, "");
193 updateConfiguration(configuration);
194 } catch (IOException | OAuthException | OAuthResponseException e) {
195 logger.debug("Error fetching access tokens. Invalid authcode! Please generate a new one. Detail: {}",
197 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
198 "Cannot connect to innogy SmartHome service. Please set auth-code!");
206 * Initializes the client and connects to the innogy SmartHome service via Client API. Based on the provided
207 * {@Link Configuration} while constructing {@Link InnogyClient}, the given oauth2 access and refresh tokens are
208 * used or - if not yet available - new tokens are fetched from the service using the provided auth code.
210 private void startClient() {
212 logger.debug("Initializing innogy SmartHome client...");
213 final InnogyClient localClient = this.client;
214 if (localClient != null) {
215 localClient.refreshStatus();
217 } catch (AuthenticationException | ApiException | IOException e) {
218 if (handleClientException(e)) {
219 // If exception could not be handled properly it's no use to continue so we won't continue start
220 logger.debug("Error initializing innogy SmartHome client.", e);
224 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
225 if (deviceStructMan == null) {
229 deviceStructMan.refreshDevices();
230 } catch (IOException | ApiException | AuthenticationException e) {
231 if (handleClientException(e)) {
232 // If exception could not be handled properly it's no use to continue so we won't continue start
233 logger.debug("Error starting device structure manager.", e);
238 Device bridgeDevice = deviceStructMan.getBridgeDevice();
239 if (bridgeDevice == null) {
240 logger.debug("Failed to get bridge device, re-scheduling startClient.");
241 scheduleRestartClient(true);
244 setBridgeProperties(bridgeDevice);
245 bridgeId = bridgeDevice.getId();
250 * Start the websocket connection for receiving permanent update {@link Event}s from the innogy API.
252 private void startWebsocket() {
254 InnogyWebSocket localWebSocket = createWebSocket();
256 if (this.webSocket != null && this.webSocket.isRunning()) {
257 this.webSocket.stop();
258 this.webSocket = null;
261 logger.debug("Starting innogy websocket.");
262 this.webSocket = localWebSocket;
263 localWebSocket.start();
264 updateStatus(ThingStatus.ONLINE);
265 } catch (final Exception e) { // Catch Exception because websocket start throws Exception
266 logger.warn("Error starting websocket.", e);
267 handleClientException(e);
271 InnogyWebSocket createWebSocket() throws IOException, AuthenticationException {
272 final AccessTokenResponse accessTokenResponse = client.getAccessTokenResponse();
273 final String webSocketUrl = WEBSOCKET_API_URL_EVENTS.replace("{token}", accessTokenResponse.getAccessToken());
275 logger.debug("WebSocket URL: {}...{}", webSocketUrl.substring(0, 70),
276 webSocketUrl.substring(webSocketUrl.length() - 10));
278 return new InnogyWebSocket(this, URI.create(webSocketUrl), bridgeConfiguration.websocketidletimeout * 1000);
282 public void onAccessTokenResponse(final AccessTokenResponse credential) {
283 scheduleRestartClient(true);
287 * Schedules a re-initialization in the given future.
289 * @param delayed when it is scheduled delayed, it starts with a delay of
290 * {@link org.openhab.binding.innogysmarthome.internal.InnogyBindingConstants#REINITIALIZE_DELAY_SECONDS}
292 * otherwise it starts directly
294 private synchronized void scheduleRestartClient(final boolean delayed) {
296 final ScheduledFuture<?> localReinitJob = reinitJob;
298 if (localReinitJob != null && isAlreadyScheduled(localReinitJob)) {
299 logger.debug("Scheduling reinitialize - ignored: already triggered in {} seconds.",
300 localReinitJob.getDelay(TimeUnit.SECONDS));
304 final long seconds = delayed ? REINITIALIZE_DELAY_SECONDS : 0;
305 logger.debug("Scheduling reinitialize in {} seconds.", seconds);
306 reinitJob = getScheduler().schedule(this::startClient, seconds, TimeUnit.SECONDS);
309 private void setBridgeProperties(final Device bridgeDevice) {
310 final DeviceConfig config = bridgeDevice.getConfig();
312 logger.debug("Setting Bridge Device Properties for Bridge of type '{}' with ID '{}'", config.getName(),
313 bridgeDevice.getId());
314 final Map<String, String> properties = editProperties();
316 setPropertyIfPresent(Thing.PROPERTY_VENDOR, bridgeDevice.getManufacturer(), properties);
317 setPropertyIfPresent(Thing.PROPERTY_SERIAL_NUMBER, bridgeDevice.getSerialnumber(), properties);
318 setPropertyIfPresent(PROPERTY_ID, bridgeDevice.getId(), properties);
319 setPropertyIfPresent(Thing.PROPERTY_FIRMWARE_VERSION, config.getFirmwareVersion(), properties);
320 setPropertyIfPresent(Thing.PROPERTY_HARDWARE_VERSION, config.getHardwareVersion(), properties);
321 setPropertyIfPresent(PROPERTY_SOFTWARE_VERSION, config.getSoftwareVersion(), properties);
322 setPropertyIfPresent(PROPERTY_IP_ADDRESS, config.getIPAddress(), properties);
323 setPropertyIfPresent(Thing.PROPERTY_MAC_ADDRESS, config.getMACAddress(), properties);
324 if (config.getRegistrationTime() != null) {
325 properties.put(PROPERTY_REGISTRATION_TIME,
326 config.getRegistrationTime().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)));
328 setPropertyIfPresent(PROPERTY_CONFIGURATION_STATE, config.getConfigurationState(), properties);
329 setPropertyIfPresent(PROPERTY_SHC_TYPE, bridgeDevice.getType(), properties);
330 setPropertyIfPresent(PROPERTY_TIME_ZONE, config.getTimeZone(), properties);
331 setPropertyIfPresent(PROPERTY_PROTOCOL_ID, config.getProtocolId(), properties);
332 setPropertyIfPresent(PROPERTY_GEOLOCATION, config.getGeoLocation(), properties);
333 setPropertyIfPresent(PROPERTY_CURRENT_UTC_OFFSET, config.getCurrentUTCOffset(), properties);
334 setPropertyIfPresent(PROPERTY_BACKEND_CONNECTION_MONITORED, config.getBackendConnectionMonitored(), properties);
335 setPropertyIfPresent(PROPERTY_RFCOM_FAILURE_NOTIFICATION, config.getRFCommFailureNotification(), properties);
336 updateProperties(properties);
339 private void setPropertyIfPresent(final String key, final @Nullable Object data,
340 final Map<String, String> properties) {
342 properties.put(key, data instanceof String ? (String) data : data.toString());
347 public void dispose() {
348 logger.debug("Disposing innogy SmartHome bridge handler '{}'", getThing().getUID().getId());
349 unregisterDeviceStatusListener(this);
351 if (webSocket != null) {
356 deviceStructMan = null;
359 logger.debug("innogy SmartHome bridge handler shut down.");
362 private synchronized void cancelReinitJob() {
363 ScheduledFuture<?> reinitJob = this.reinitJob;
365 if (reinitJob != null) {
366 reinitJob.cancel(true);
367 this.reinitJob = null;
372 * Registers a {@link DeviceStatusListener}.
374 * @param deviceStatusListener
375 * @return true, if successful
377 public boolean registerDeviceStatusListener(final DeviceStatusListener deviceStatusListener) {
378 return deviceStatusListeners.add(deviceStatusListener);
382 * Unregisters a {@link DeviceStatusListener}.
384 * @param deviceStatusListener
385 * @return true, if successful
387 public boolean unregisterDeviceStatusListener(final DeviceStatusListener deviceStatusListener) {
388 return deviceStatusListeners.remove(deviceStatusListener);
392 * Loads a Collection of {@link Device}s from the bridge and returns them.
394 * @return a Collection of {@link Device}s
396 public Collection<Device> loadDevices() {
397 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
398 final Collection<Device> devices;
400 if (deviceStructMan == null) {
401 devices = Collections.emptyList();
403 devices = deviceStructMan.getDeviceList();
409 * Returns the {@link Device} with the given deviceId.
412 * @return {@link Device} or null, if it does not exist or no {@link DeviceStructureManager} is available
414 public @Nullable Device getDeviceById(final String deviceId) {
415 if (deviceStructMan != null) {
416 return deviceStructMan.getDeviceById(deviceId);
422 * Refreshes the {@link Device} with the given id, by reloading the full device from the innogy webservice.
425 * @return the {@link Device} or null, if it does not exist or no {@link DeviceStructureManager} is available
427 public @Nullable Device refreshDevice(final String deviceId) {
428 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
429 if (deviceStructMan == null) {
433 Device device = null;
435 deviceStructMan.refreshDevice(deviceId);
436 device = deviceStructMan.getDeviceById(deviceId);
437 } catch (IOException | ApiException | AuthenticationException e) {
438 handleClientException(e);
444 public void onDeviceStateChanged(final Device device) {
445 synchronized (this.lock) {
446 if (!bridgeId.equals(device.getId())) {
447 logger.trace("DeviceId {} not relevant for this handler (responsible for id {})", device.getId(),
452 logger.debug("onDeviceStateChanged called with device {}/{}", device.getConfig().getName(), device.getId());
455 if (device.hasDeviceState()) {
456 final Double cpuUsage = device.getDeviceState().getState().getCpuUsage().getValue();
457 if (cpuUsage != null) {
458 logger.debug("-> CPU usage state: {}", cpuUsage);
459 updateState(CHANNEL_CPU, new DecimalType(cpuUsage));
461 final Double diskUsage = device.getDeviceState().getState().getDiskUsage().getValue();
462 if (diskUsage != null) {
463 logger.debug("-> Disk usage state: {}", diskUsage);
464 updateState(CHANNEL_DISK, new DecimalType(diskUsage));
466 final Double memoryUsage = device.getDeviceState().getState().getMemoryUsage().getValue();
467 if (memoryUsage != null) {
468 logger.debug("-> Memory usage state: {}", memoryUsage);
469 updateState(CHANNEL_MEMORY, new DecimalType(memoryUsage));
478 public void onDeviceStateChanged(final Device device, final Event event) {
479 synchronized (this.lock) {
480 if (!bridgeId.equals(device.getId())) {
481 logger.trace("DeviceId {} not relevant for this handler (responsible for id {})", device.getId(),
486 logger.trace("DeviceId {} relevant for this handler.", device.getId());
488 if (event.isLinkedtoDevice() && DEVICE_SHCA.equals(device.getType())) {
489 device.getDeviceState().getState().getCpuUsage().setValue(event.getProperties().getCpuUsage());
490 device.getDeviceState().getState().getDiskUsage().setValue(event.getProperties().getDiskUsage());
491 device.getDeviceState().getState().getMemoryUsage().setValue(event.getProperties().getMemoryUsage());
492 onDeviceStateChanged(device);
498 public void onEvent(final String msg) {
499 logger.trace("onEvent called. Msg: {}", msg);
502 final BaseEvent be = gson.fromJson(msg, BaseEvent.class);
503 logger.debug("Event no {} found. Type: {}", be.getSequenceNumber(), be.getType());
504 if (!BaseEvent.SUPPORTED_EVENT_TYPES.contains(be.getType())) {
505 logger.debug("Event type {} not supported. Skipping...", be.getType());
507 final Event event = gson.fromJson(msg, Event.class);
509 switch (event.getType()) {
510 case BaseEvent.TYPE_STATE_CHANGED:
511 case BaseEvent.TYPE_BUTTON_PRESSED:
512 handleStateChangedEvent(event);
515 case BaseEvent.TYPE_DISCONNECT:
516 logger.debug("Websocket disconnected.");
517 scheduleRestartClient(true);
520 case BaseEvent.TYPE_CONFIGURATION_CHANGED:
521 if (client.getConfigVersion().equals(event.getConfigurationVersion().toString())) {
523 "Ignored configuration changed event with version '{}' as current version is '{}' the same.",
524 event.getConfigurationVersion(), client.getConfigVersion());
526 logger.info("Configuration changed from version {} to {}. Restarting innogy binding...",
527 client.getConfigVersion(), event.getConfigurationVersion());
528 scheduleRestartClient(false);
532 case BaseEvent.TYPE_CONTROLLER_CONNECTIVITY_CHANGED:
533 handleControllerConnectivityChangedEvent(event);
536 case BaseEvent.TYPE_NEW_MESSAGE_RECEIVED:
537 case BaseEvent.TYPE_MESSAGE_CREATED:
538 final MessageEvent messageEvent = gson.fromJson(msg, MessageEvent.class);
539 handleNewMessageReceivedEvent(Objects.requireNonNull(messageEvent));
542 case BaseEvent.TYPE_MESSAGE_DELETED:
543 handleMessageDeletedEvent(event);
547 logger.debug("Unsupported eventtype {}.", event.getType());
551 } catch (IOException | ApiException | AuthenticationException | RuntimeException e) {
552 logger.debug("Error with Event: {}", e.getMessage(), e);
553 handleClientException(e);
558 public void onError(final Throwable cause) {
559 if (cause instanceof Exception) {
560 handleClientException((Exception) cause);
565 * Handles the event that occurs, when the state of a device (like reachability) or a capability (like a temperature
566 * value) has changed.
569 * @throws ApiException
570 * @throws IOException
571 * @throws AuthenticationException
573 public void handleStateChangedEvent(final Event event) throws ApiException, IOException, AuthenticationException {
574 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
575 if (deviceStructMan == null) {
580 if (event.isLinkedtoCapability()) {
581 logger.trace("Event is linked to capability");
582 final Device device = deviceStructMan.getDeviceByCapabilityId(event.getSourceId());
583 if (device != null) {
584 for (final DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
585 deviceStatusListener.onDeviceStateChanged(device, event);
588 logger.debug("Unknown/unsupported device for capability {}.", event.getSource());
592 } else if (event.isLinkedtoDevice()) {
593 logger.trace("Event is linked to device");
595 if (!event.getSourceId().equals(deviceStructMan.getBridgeDevice().getId())) {
596 deviceStructMan.refreshDevice(event.getSourceId());
598 final Device device = deviceStructMan.getDeviceById(event.getSourceId());
599 if (device != null) {
600 for (final DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
601 deviceStatusListener.onDeviceStateChanged(device, event);
604 logger.debug("Unknown/unsupported device {}.", event.getSourceId());
608 logger.debug("link type {} not supported (yet?)", event.getSourceLinkType());
613 * Handles the event that occurs, when the connectivity of the bridge has changed.
616 * @throws ApiException
617 * @throws IOException
618 * @throws AuthenticationException
620 public void handleControllerConnectivityChangedEvent(final Event event)
621 throws ApiException, IOException, AuthenticationException {
622 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
623 if (deviceStructMan == null) {
626 final Boolean connected = event.getIsConnected();
627 if (connected != null) {
628 logger.debug("SmartHome Controller connectivity changed to {}.", connected ? "online" : "offline");
630 deviceStructMan.refreshDevices();
631 updateStatus(ThingStatus.ONLINE);
633 updateStatus(ThingStatus.OFFLINE);
636 logger.warn("isConnected property missing in event! (returned null)");
641 * Handles the event that occurs, when a new message was received. Currently only handles low battery messages.
644 * @throws ApiException
645 * @throws IOException
646 * @throws AuthenticationException
648 public void handleNewMessageReceivedEvent(final MessageEvent event)
649 throws ApiException, IOException, AuthenticationException {
650 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
651 if (deviceStructMan == null) {
654 final Message message = event.getMessage();
655 if (logger.isTraceEnabled()) {
656 logger.trace("Message: {}", gson.toJson(message));
657 logger.trace("Messagetype: {}", message.getType());
659 if (Message.TYPE_DEVICE_LOW_BATTERY.equals(message.getType()) && message.getDevices() != null) {
660 for (final String link : message.getDevices()) {
661 deviceStructMan.refreshDevice(Link.getId(link));
662 final Device device = deviceStructMan.getDeviceById(Link.getId(link));
663 if (device != null) {
664 for (final DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
665 deviceStatusListener.onDeviceStateChanged(device);
668 logger.debug("Unknown/unsupported device {}.", event.getSourceId());
672 logger.debug("Message received event not yet implemented for Messagetype {}.", message.getType());
677 * Handle the event that occurs, when a message was deleted. In case of a low battery message this means, that the
678 * device is back to normal. Currently, only messages linked to devices are handled by refreshing the device data
679 * and informing the {@link InnogyDeviceHandler} about the changed device.
682 * @throws ApiException
683 * @throws IOException
684 * @throws AuthenticationException
686 public void handleMessageDeletedEvent(final Event event) throws ApiException, IOException, AuthenticationException {
687 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
688 if (deviceStructMan == null) {
691 final String messageId = event.getData().getId();
693 logger.debug("handleMessageDeletedEvent with messageId '{}'", messageId);
694 Device device = deviceStructMan.getDeviceWithMessageId(messageId);
696 if (device != null) {
697 String id = device.getId();
698 deviceStructMan.refreshDevice(id);
699 device = deviceStructMan.getDeviceById(id);
700 if (device != null) {
701 for (final DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
702 deviceStatusListener.onDeviceStateChanged(device);
705 logger.debug("No device with id {} found after refresh.", id);
708 logger.debug("No device found with message id {}.", messageId);
713 public void connectionClosed() {
714 scheduleRestartClient(true);
718 * Sends the command to switch the {@link Device} with the given id to the new state. Is called by the
719 * {@link InnogyDeviceHandler} for switch devices like the VariableActuator, PSS, PSSO or ISS2.
724 public void commandSwitchDevice(final String deviceId, final boolean state) {
725 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
726 if (deviceStructMan == null) {
731 final String deviceType = deviceStructMan.getDeviceById(deviceId).getType();
732 if (DEVICE_VARIABLE_ACTUATOR.equals(deviceType)) {
733 final String capabilityId = deviceStructMan.getCapabilityId(deviceId, Capability.TYPE_VARIABLEACTUATOR);
734 if (capabilityId == null) {
737 client.setVariableActuatorState(capabilityId, state);
740 } else if (DEVICE_PSS.equals(deviceType) || DEVICE_PSSO.equals(deviceType)
741 || DEVICE_ISS2.equals(deviceType)) {
742 final String capabilityId = deviceStructMan.getCapabilityId(deviceId, Capability.TYPE_SWITCHACTUATOR);
743 if (capabilityId == null) {
746 client.setSwitchActuatorState(capabilityId, state);
748 } catch (IOException | ApiException | AuthenticationException e) {
749 handleClientException(e);
754 * Sends the command to update the point temperature of the {@link Device} with the given deviceId. Is called by the
755 * {@link InnogyDeviceHandler} for thermostat {@link Device}s like RST or WRT.
758 * @param pointTemperature
760 public void commandUpdatePointTemperature(final String deviceId, final double pointTemperature) {
761 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
762 if (deviceStructMan == null) {
766 final String capabilityId = deviceStructMan.getCapabilityId(deviceId, Capability.TYPE_THERMOSTATACTUATOR);
767 if (capabilityId == null) {
770 client.setPointTemperatureState(capabilityId, pointTemperature);
771 } catch (IOException | ApiException | AuthenticationException e) {
772 handleClientException(e);
777 * Sends the command to turn the alarm of the {@link Device} with the given id on or off. Is called by the
778 * {@link InnogyDeviceHandler} for smoke detector {@link Device}s like WSD or WSD2.
783 public void commandSwitchAlarm(final String deviceId, final boolean alarmState) {
784 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
785 if (deviceStructMan == null) {
789 final String capabilityId = deviceStructMan.getCapabilityId(deviceId, Capability.TYPE_ALARMACTUATOR);
790 if (capabilityId == null) {
793 client.setAlarmActuatorState(capabilityId, alarmState);
794 } catch (IOException | ApiException | AuthenticationException e) {
795 handleClientException(e);
800 * Sends the command to set the operation mode of the {@link Device} with the given deviceId to auto (or manual, if
801 * false). Is called by the {@link InnogyDeviceHandler} for thermostat {@link Device}s like RST.
804 * @param autoMode true activates the automatic mode, false the manual mode.
806 public void commandSetOperationMode(final String deviceId, final boolean autoMode) {
807 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
808 if (deviceStructMan == null) {
812 final String capabilityId = deviceStructMan.getCapabilityId(deviceId, Capability.TYPE_THERMOSTATACTUATOR);
813 if (capabilityId == null) {
816 client.setOperationMode(capabilityId, autoMode);
817 } catch (IOException | ApiException | AuthenticationException e) {
818 handleClientException(e);
823 * Sends the command to set the dimm level of the {@link Device} with the given id. Is called by the
824 * {@link InnogyDeviceHandler} for {@link Device}s like ISD2 or PSD.
829 public void commandSetDimmLevel(final String deviceId, final int dimLevel) {
830 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
831 if (deviceStructMan == null) {
835 final String capabilityId = deviceStructMan.getCapabilityId(deviceId, Capability.TYPE_DIMMERACTUATOR);
836 if (capabilityId == null) {
839 client.setDimmerActuatorState(capabilityId, dimLevel);
840 } catch (IOException | ApiException | AuthenticationException e) {
841 handleClientException(e);
846 * Sends the command to set the rollershutter level of the {@link Device} with the given id. Is called by the
847 * {@link InnogyDeviceHandler} for {@link Device}s like ISR2.
850 * @param rollerSchutterLevel
852 public void commandSetRollerShutterLevel(final String deviceId, final int rollerSchutterLevel) {
853 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
854 if (deviceStructMan == null) {
858 final String capabilityId = deviceStructMan.getCapabilityId(deviceId,
859 Capability.TYPE_ROLLERSHUTTERACTUATOR);
860 if (capabilityId == null) {
863 client.setRollerShutterActuatorState(capabilityId, rollerSchutterLevel);
864 } catch (IOException | ApiException | AuthenticationException e) {
865 handleClientException(e);
870 * Sends the command to start or stop moving the rollershutter (ISR2) in a specified direction
875 public void commandSetRollerShutterStop(final String deviceId, ShutterAction.ShutterActions action) {
876 final DeviceStructureManager deviceStructMan = this.deviceStructMan;
877 if (deviceStructMan == null) {
881 final String capabilityId = deviceStructMan.getCapabilityId(deviceId,
882 Capability.TYPE_ROLLERSHUTTERACTUATOR);
883 if (capabilityId == null) {
886 client.setRollerShutterAction(capabilityId, action);
887 } catch (IOException | ApiException | AuthenticationException e) {
888 handleClientException(e);
892 ScheduledExecutorService getScheduler() {
896 FullDeviceManager createFullDeviceManager(InnogyClient client) {
897 return new FullDeviceManager(client);
900 InnogyClient createInnogyClient(final OAuthClientService oAuthService, final HttpClient httpClient) {
901 return new InnogyClient(oAuthService, httpClient);
905 * Handles all Exceptions of the client communication. For minor "errors" like an already existing session, it
906 * returns true to inform the binding to continue running. In other cases it may e.g. schedule a reinitialization of
909 * @param e the Exception
910 * @return boolean true, if binding should continue.
912 private boolean handleClientException(final Exception e) {
913 boolean isReinitialize = true;
914 if (e instanceof SessionExistsException) {
915 logger.debug("Session already exists. Continuing...");
916 isReinitialize = false;
917 } else if (e instanceof InvalidActionTriggeredException) {
918 logger.debug("Error triggering action: {}", e.getMessage());
919 isReinitialize = false;
920 } else if (e instanceof RemoteAccessNotAllowedException) {
921 // Remote access not allowed (usually by IP address change)
922 logger.debug("Remote access not allowed. Dropping access token and reinitializing binding...");
923 refreshAccessToken();
924 } else if (e instanceof ControllerOfflineException) {
925 logger.debug("innogy SmartHome Controller is offline.");
926 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, e.getMessage());
927 } else if (e instanceof AuthenticationException) {
928 logger.debug("OAuthenticaton error, refreshing tokens: {}", e.getMessage());
929 refreshAccessToken();
930 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
931 } else if (e instanceof IOException) {
932 logger.debug("IO error: {}", e.getMessage());
933 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
934 } else if (e instanceof ApiException) {
935 logger.warn("Unexpected API error: {}", e.getMessage());
936 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
937 } else if (e instanceof TimeoutException) {
938 logger.debug("WebSocket timeout: {}", e.getMessage());
939 } else if (e instanceof SocketTimeoutException) {
940 logger.debug("Socket timeout: {}", e.getMessage());
941 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
942 } else if (e instanceof InterruptedException) {
943 isReinitialize = false;
944 Thread.currentThread().interrupt();
945 } else if (e instanceof ExecutionException) {
946 logger.debug("ExecutionException: {}", ExceptionUtils.getRootCauseMessage(e));
947 updateStatus(ThingStatus.OFFLINE);
949 logger.debug("Unknown exception", e);
950 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
952 if (isReinitialize) {
953 scheduleRestartClient(true);
959 private void refreshAccessToken() {
961 final OAuthClientService localOAuthService = this.oAuthService;
963 if (localOAuthService != null) {
964 oAuthService.refreshToken();
966 } catch (IOException | OAuthResponseException | OAuthException e) {
967 logger.debug("Could not refresh tokens", e);
972 * Checks if the job is already (re-)scheduled.
974 * @param job job to check
975 * @return true, when the job is already (re-)scheduled, otherwise false
977 private static boolean isAlreadyScheduled(ScheduledFuture<?> job) {
978 return job.getDelay(TimeUnit.SECONDS) > 0;