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.digitalstrom.internal.lib.event;
15 import java.util.ArrayList;
16 import java.util.Collections;
17 import java.util.LinkedList;
18 import java.util.List;
19 import java.util.concurrent.ScheduledExecutorService;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
23 import org.openhab.binding.digitalstrom.internal.lib.config.Config;
24 import org.openhab.binding.digitalstrom.internal.lib.event.types.Event;
25 import org.openhab.binding.digitalstrom.internal.lib.event.types.EventItem;
26 import org.openhab.binding.digitalstrom.internal.lib.event.types.JSONEventImpl;
27 import org.openhab.binding.digitalstrom.internal.lib.manager.ConnectionManager;
28 import org.openhab.binding.digitalstrom.internal.lib.serverconnection.constants.JSONApiResponseKeysEnum;
29 import org.openhab.binding.digitalstrom.internal.lib.serverconnection.impl.JSONResponseHandler;
30 import org.openhab.core.common.ThreadPoolManager;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
34 import com.google.gson.JsonArray;
35 import com.google.gson.JsonObject;
38 * The {@link EventListener} listens for events which will be thrown by the digitalSTROM-Server and it notifies the
39 * added {@link EventHandler} about the detected events, if it supports the event-type.<br>
40 * You can add {@link EventHandler}'s through the constructors or the methods {@link #addEventHandler(EventHandler)} and
41 * {@link #addEventHandlers(List)}.<br>
42 * You can also delete an {@link EventHandler} though the method {@link #removeEventHandler(EventHandler)}.<br>
43 * If the {@link EventListener} is started, both methods subscribe respectively unsubscribe the event-types of the
44 * {@link EventHandler}/s automatically.<br>
45 * If you want to dynamically subscribe event-types, e.g. because a configuration has changed and a
46 * {@link EventHandler} needs to be informed of another event-type, you can use the methods
47 * {@link #addSubscribe(String)} or {@link #addSubscribeEvents(List)} to add more than one event-type. To remove a
48 * subscribed event you can use the method {@link #removeSubscribe(String, String)}, you also have to change the return
49 * of the {@link org.openhab.binding.digitalstrom.internal.lib.event.EventHandler} methods
50 * {@link EventHandler#getSupportedEvents()} and
51 * {@link EventHandler#supportsEvent(String)}.
53 * To start and stop the listening you have to call the methods {@link #start()} and {@link #stop()}.
55 * @author Michael Ochel - Initial contribution
56 * @author Matthias Siegele - Initial contribution
58 public class EventListener {
60 private final Logger logger = LoggerFactory.getLogger(EventListener.class);
61 private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(Config.THREADPOOL_NAME);
62 public static final int SUBSCRIBE_DELAY = 500;
63 private ScheduledFuture<?> pollingScheduler;
64 private ScheduledFuture<?> subscriptionScheduler;
66 private int subscriptionID = 15;
67 private final int timeout = 500;
68 private final List<String> subscribedEvents = Collections.synchronizedList(new LinkedList<>());
69 private boolean subscribed = false;
72 public static final String INVALID_SESSION = "Invalid session!";
73 public static final String TOKEN_NOT_FOUND = "token not found."; // Complete text: "Event subscription token not
76 private final ConnectionManager connManager;
77 private final List<EventHandler> eventHandlers = Collections.synchronizedList(new LinkedList<>());
78 private final Config config;
79 private boolean isStarted = false;
82 * Creates a new {@link EventListener} to listen to the supported event-types of the given eventHandler and notify
83 * about a detected event.<br>
85 * To get notified by events you have to call {@link #start()}.
87 * @param connectionManager must not be null
88 * @param eventHandler must not be null
90 public EventListener(ConnectionManager connectionManager, EventHandler eventHandler) {
91 this.connManager = connectionManager;
92 this.config = connectionManager.getConfig();
93 addEventHandler(eventHandler);
97 * This constructor can add more than one {@link EventHandler} as a list of {@link EventHandler}'s.
99 * @param connectionManager must not be null
100 * @param eventHandlers list of {@link EventHandler}'s must not be null
101 * @see #EventListener(ConnectionManager, EventHandler)
103 public EventListener(ConnectionManager connectionManager, List<EventHandler> eventHandlers) {
104 this.connManager = connectionManager;
105 this.config = connectionManager.getConfig();
106 addEventHandlers(eventHandlers);
110 * Creates a new {@link EventListener} without an {@link EventHandler}<br>
112 * To get notified by events you have to call {@link #start()} and {@link #addEventHandler(EventHandler)} or
113 * {@link #addEventHandlers(List)}.
115 * @param connectionManager must not be null
117 public EventListener(ConnectionManager connectionManager) {
118 this.connManager = connectionManager;
119 this.config = connectionManager.getConfig();
123 * Stops this {@link EventListener} and unsubscribe events.
125 public synchronized void stop() {
126 logger.debug("Stop EventListener");
132 * Returns true, if the {@link EventListener} is started.
134 * @return true, if is started
136 public boolean isStarted() {
140 private void stopSubscriptionScheduler() {
141 final ScheduledFuture<?> subscriptionScheduler = this.subscriptionScheduler;
142 if (subscriptionScheduler != null) {
143 subscriptionScheduler.cancel(true);
144 this.subscriptionScheduler = null;
148 private void internalStop() {
149 stopSubscriptionScheduler();
151 ScheduledFuture<?> pollingScheduler = this.pollingScheduler;
152 if (pollingScheduler != null) {
153 pollingScheduler.cancel(true);
154 this.pollingScheduler = null;
156 logger.debug("internal stop EventListener");
161 * Starts this {@link EventListener} and subscribe events.
163 public synchronized void start() {
164 logger.debug("Start EventListener");
169 private void internalStart() {
170 if (!eventHandlers.isEmpty() && (pollingScheduler == null || pollingScheduler.isCancelled())) {
171 pollingScheduler = scheduler.scheduleWithFixedDelay(runableListener, 0,
172 config.getEventListenerRefreshinterval(), TimeUnit.MILLISECONDS);
173 logger.debug("internal start EventListener");
178 * Adds a {@link List} of {@link EventHandler}'s and subscribe the supported event-types, if the
179 * {@link EventListener} is started and the event-types are not already subscribed.
181 * @param eventHandlers to add
183 public void addEventHandlers(List<EventHandler> eventHandlers) {
184 if (eventHandlers != null) {
185 for (EventHandler eventHandler : eventHandlers) {
186 addEventHandler(eventHandler);
192 * Adds an {@link EventHandler}'s and subscribe the supported event-types, if the
193 * {@link EventListener} is started and the event-types are not already subscribed.<br>
196 * If {@link #start()} was called before the {@link EventListener} will start now, otherwise you have to call
197 * {@link #start()} to get notified by events.
199 * @param eventHandler to add
201 public void addEventHandler(EventHandler eventHandler) {
202 if (eventHandler != null) {
203 boolean handlerExist = false;
204 for (EventHandler handler : eventHandlers) {
205 if (handler.getUID().equals(eventHandler.getUID())) {
210 eventHandlers.add(eventHandler);
211 addSubscribeEvents(eventHandler.getSupportedEvents());
212 logger.debug("eventHandler: {} added", eventHandler.getUID());
221 * Remove an {@link EventHandler} and unsubscribes the supported event-types, if the
222 * {@link EventListener} is started and no other {@link EventHandler} needed the event-types.
224 * @param eventHandler to remove
226 public void removeEventHandler(EventHandler eventHandler) {
227 if (eventHandler != null && eventHandlers.contains(eventHandler)) {
228 List<String> tempSubsList = new ArrayList<>();
230 EventHandler intEventHandler = null;
231 boolean subscribedEventsChanged = false;
232 for (int i = 0; i < eventHandlers.size(); i++) {
233 intEventHandler = eventHandlers.get(i);
234 if (intEventHandler.getUID().equals(eventHandler.getUID())) {
237 tempSubsList.addAll(intEventHandler.getSupportedEvents());
241 intEventHandler = eventHandlers.remove(index);
242 for (String eventName : intEventHandler.getSupportedEvents()) {
243 if (!tempSubsList.contains(eventName)) {
244 subscribedEvents.remove(eventName);
245 subscribedEventsChanged = true;
249 if (subscribedEventsChanged) {
250 // Because of the json-call unsubscribe?eventName=XY&subscriptionID=Z doesn't work like it is explained
251 // in the dS-JSON-API, the whole EventListener will be restarted. The problem is, that not only the
252 // given eventName, rather all events of the subscitionID will be deleted.
259 * Removes a subscribed event and unsubscibe it, if it is not needed by other {@link EventHandler}'s.
261 * @param unsubscribeEvent event name to unsubscibe
262 * @param eventHandlerID EventHandler-ID of the EventHandler that unsubscibe an event
264 public void removeSubscribe(String unsubscribeEvent, String eventHandlerID) {
265 if (subscribedEvents != null && !subscribedEvents.contains(unsubscribeEvent)) {
266 boolean eventNeededByAnotherHandler = false;
267 for (EventHandler handler : eventHandlers) {
268 if (!handler.getUID().equals(eventHandlerID)) {
269 eventNeededByAnotherHandler = handler.getSupportedEvents().contains(unsubscribeEvent);
272 if (!eventNeededByAnotherHandler) {
273 logger.debug("unsubscribeEvent: {} is not needed by other EventHandler's... unsubscribe it",
275 subscribedEvents.remove(unsubscribeEvent);
278 logger.debug("unsubscribeEvent: {} is needed by other EventHandler's... dosen't unsubscribe it",
284 private void restartListener() {
286 if (!eventHandlers.isEmpty() && isStarted) {
287 logger.debug("Min one subscribed events was deleted, EventListener will be restarted");
293 * Adds an event and subscribes it, if it is not subscribed already.
295 * @param subscribeEvent event name to subscribe
297 public void addSubscribe(String subscribeEvent) {
298 if (!subscribedEvents.contains(subscribeEvent)) {
299 subscribedEvents.add(subscribeEvent);
300 logger.debug("subscibeEvent: {} added", subscribeEvent);
302 subscribe(subscribeEvent);
308 * Adds the events of the {@link List} and subscribe them, if an event is not subscribed already.
310 * @param subscribeEvents event name to subscribe
312 public void addSubscribeEvents(List<String> subscribeEvents) {
313 for (String eventName : subscribeEvents) {
314 subscribe(eventName);
318 private void getSubscriptionID() {
319 boolean subscriptionIDavailable = false;
320 while (!subscriptionIDavailable) {
321 String response = connManager.getDigitalSTROMAPI().getEvent(connManager.getSessionToken(), subscriptionID,
324 JsonObject responseObj = JSONResponseHandler.toJsonObject(response);
326 if (JSONResponseHandler.checkResponse(responseObj)) {
329 String errorStr = null;
330 if (responseObj != null && responseObj.get(JSONApiResponseKeysEnum.MESSAGE.getKey()) != null) {
331 errorStr = responseObj.get(JSONApiResponseKeysEnum.MESSAGE.getKey()).getAsString();
333 if (errorStr != null && errorStr.contains(TOKEN_NOT_FOUND)) {
334 subscriptionIDavailable = true;
340 private boolean subscribe(String eventName) {
344 subscribed = connManager.getDigitalSTROMAPI().subscribeEvent(connManager.getSessionToken(), eventName,
345 subscriptionID, config.getConnectionTimeout(), config.getReadTimeout());
348 logger.debug("subscribed event: {} to subscriptionID: {}", eventName, subscriptionID);
351 "Couldn't subscribe event {} ... maybe timeout because system is too busy ... event will be tried to subscribe later again ... ",
357 private void subscribe(final List<String> eventNames) {
358 subscriptionScheduler = scheduler.scheduleWithFixedDelay(() -> {
359 eventNames.forEach(this::subscribe);
360 stopSubscriptionScheduler();
361 }, 0, SUBSCRIBE_DELAY, TimeUnit.MILLISECONDS);
364 private final Runnable runableListener = new Runnable() {
369 String response = connManager.getDigitalSTROMAPI().getEvent(connManager.getSessionToken(),
370 subscriptionID, timeout);
371 JsonObject responseObj = JSONResponseHandler.toJsonObject(response);
373 if (JSONResponseHandler.checkResponse(responseObj)) {
374 JsonObject obj = JSONResponseHandler.getResultJsonObject(responseObj);
375 if (obj != null && obj.get(JSONApiResponseKeysEnum.EVENTS.getKey()).isJsonArray()) {
376 JsonArray array = obj.get(JSONApiResponseKeysEnum.EVENTS.getKey()).getAsJsonArray();
380 String errorStr = null;
381 if (responseObj != null && responseObj.get(JSONApiResponseKeysEnum.MESSAGE.getKey()) != null) {
382 errorStr = responseObj.get(JSONApiResponseKeysEnum.MESSAGE.getKey()).getAsString();
384 if (errorStr != null && (errorStr.equals(INVALID_SESSION) || errorStr.contains(TOKEN_NOT_FOUND))) {
386 subscribe(subscribedEvents);
387 } else if (errorStr != null) {
388 pollingScheduler.cancel(true);
389 logger.error("Unknown error message at event response: {}", errorStr);
393 subscribe(subscribedEvents);
398 private void unsubscribe() {
399 for (String eventName : this.subscribedEvents) {
400 connManager.getDigitalSTROMAPI().unsubscribeEvent(this.connManager.getSessionToken(), eventName,
401 this.subscriptionID, config.getConnectionTimeout(), config.getReadTimeout());
405 private void handleEvent(JsonArray array) {
406 if (array.size() > 0) {
407 Event event = new JSONEventImpl(array);
408 for (EventItem item : event.getEventItems()) {
409 logger.debug("detect event {}", item.toString());
410 for (EventHandler handler : eventHandlers) {
411 if (handler.supportsEvent(item.getName())) {
412 logger.debug("inform handler with id {} about event {}", handler.getUID(), item.toString());
413 handler.handleEvent(item);