]> git.basschouten.com Git - openhab-addons.git/blob
57a3bce4a8098eeecf2a73202908eab4e7e83f5b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.digitalstrom.internal.lib.event;
14
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;
22
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;
33
34 import com.google.gson.JsonArray;
35 import com.google.gson.JsonObject;
36
37 /**
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 EventHandler} methods {@link EventHandler#getSupportetEvents()} and
50  * {@link EventHandler#supportsEvent(String)}.
51  * <br>
52  * To start and stop the listening you have to call the methods {@link #start()} and {@link #stop()}.
53  *
54  * @author Michael Ochel - Initial contribution
55  * @author Matthias Siegele - Initial contribution
56  */
57 public class EventListener {
58
59     private final Logger logger = LoggerFactory.getLogger(EventListener.class);
60     private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(Config.THREADPOOL_NAME);
61     public static final int SUBSCRIBE_DELAY = 500;
62     private ScheduledFuture<?> pollingScheduler;
63     private ScheduledFuture<?> subscriptionScheduler;
64
65     private int subscriptionID = 15;
66     private final int timeout = 500;
67     private final List<String> subscribedEvents = Collections.synchronizedList(new LinkedList<>());
68     private boolean subscribed = false;
69
70     // error message
71     public static final String INVALID_SESSION = "Invalid session!";
72     public static final String TOKEN_NOT_FOUND = "token not found."; // Complete text: "Event subscription token not
73                                                                      // found."
74
75     private final ConnectionManager connManager;
76     private final List<EventHandler> eventHandlers = Collections.synchronizedList(new LinkedList<>());
77     private final Config config;
78     private boolean isStarted = false;
79
80     /**
81      * Creates a new {@link EventListener} to listen to the supported event-types of the given eventHandler and notify
82      * about a detected event.<br>
83      * <br>
84      * To get notified by events you have to call {@link #start()}.
85      *
86      * @param connectionManager must not be null
87      * @param eventHandler must not be null
88      */
89     public EventListener(ConnectionManager connectionManager, EventHandler eventHandler) {
90         this.connManager = connectionManager;
91         this.config = connectionManager.getConfig();
92         addEventHandler(eventHandler);
93     }
94
95     /**
96      * This constructor can add more than one {@link EventHandler} as a list of {@link EventHandler}'s.
97      *
98      * @param connectionManager must not be null
99      * @param eventHandlers list of {@link EventHandler}'s must not be null
100      * @see #EventListener(ConnectionManager, EventHandler)
101      */
102     public EventListener(ConnectionManager connectionManager, List<EventHandler> eventHandlers) {
103         this.connManager = connectionManager;
104         this.config = connectionManager.getConfig();
105         addEventHandlers(eventHandlers);
106     }
107
108     /**
109      * Creates a new {@link EventListener} without an {@link EventHandler}<br>
110      * <br>
111      * To get notified by events you have to call {@link #start()} and {@link #addEventHandler(EventHandler)} or
112      * {@link #addEventHandlers(List)}.
113      *
114      * @param connectionManager must not be null
115      */
116     public EventListener(ConnectionManager connectionManager) {
117         this.connManager = connectionManager;
118         this.config = connectionManager.getConfig();
119     }
120
121     /**
122      * Stops this {@link EventListener} and unsubscribe events.
123      */
124     public synchronized void stop() {
125         logger.debug("Stop EventListener");
126         isStarted = false;
127         internalStop();
128     }
129
130     /**
131      * Returns true, if the {@link EventListener} is started.
132      *
133      * @return true, if is started
134      */
135     public boolean isStarted() {
136         return isStarted;
137     }
138
139     private void stopSubscriptionScheduler() {
140         final ScheduledFuture<?> subscriptionScheduler = this.subscriptionScheduler;
141         if (subscriptionScheduler != null) {
142             subscriptionScheduler.cancel(true);
143             this.subscriptionScheduler = null;
144         }
145     }
146
147     private void internalStop() {
148         stopSubscriptionScheduler();
149
150         ScheduledFuture<?> pollingScheduler = this.pollingScheduler;
151         if (pollingScheduler != null) {
152             pollingScheduler.cancel(true);
153             this.pollingScheduler = null;
154             unsubscribe();
155             logger.debug("internal stop EventListener");
156         }
157     }
158
159     /**
160      * Starts this {@link EventListener} and subscribe events.
161      */
162     public synchronized void start() {
163         logger.debug("Start EventListener");
164         isStarted = true;
165         internalStart();
166     }
167
168     private void internalStart() {
169         if (!eventHandlers.isEmpty() && (pollingScheduler == null || pollingScheduler.isCancelled())) {
170             pollingScheduler = scheduler.scheduleWithFixedDelay(runableListener, 0,
171                     config.getEventListenerRefreshinterval(), TimeUnit.MILLISECONDS);
172             logger.debug("internal start EventListener");
173         }
174     }
175
176     /**
177      * Adds a {@link List} of {@link EventHandler}'s and subscribe the supported event-types, if the
178      * {@link EventListener} is started and the event-types are not already subscribed.
179      *
180      * @param eventHandlers to add
181      */
182     public void addEventHandlers(List<EventHandler> eventHandlers) {
183         if (eventHandlers != null) {
184             for (EventHandler eventHandler : eventHandlers) {
185                 addEventHandler(eventHandler);
186             }
187         }
188     }
189
190     /**
191      * Adds an {@link EventHandler}'s and subscribe the supported event-types, if the
192      * {@link EventListener} is started and the event-types are not already subscribed.<br>
193      * <br>
194      * <b>Note:</b><br>
195      * If {@link #start()} was called before the {@link EventListener} will start now, otherwise you have to call
196      * {@link #start()} to get notified by events.
197      *
198      * @param eventHandler to add
199      */
200     public void addEventHandler(EventHandler eventHandler) {
201         if (eventHandler != null) {
202             boolean handlerExist = false;
203             for (EventHandler handler : eventHandlers) {
204                 if (handler.getUID().equals(eventHandler.getUID())) {
205                     handlerExist = true;
206                 }
207             }
208             if (!handlerExist) {
209                 eventHandlers.add(eventHandler);
210                 addSubscribeEvents(eventHandler.getSupportedEvents());
211                 logger.debug("eventHandler: {} added", eventHandler.getUID());
212                 if (isStarted) {
213                     internalStart();
214                 }
215             }
216         }
217     }
218
219     /**
220      * Remove an {@link EventHandler} and unsubscribes the supported event-types, if the
221      * {@link EventListener} is started and no other {@link EventHandler} needed the event-types.
222      *
223      * @param eventHandler to remove
224      */
225     public void removeEventHandler(EventHandler eventHandler) {
226         if (eventHandler != null && eventHandlers.contains(eventHandler)) {
227             List<String> tempSubsList = new ArrayList<>();
228             int index = -1;
229             EventHandler intEventHandler = null;
230             boolean subscribedEventsChanged = false;
231             for (int i = 0; i < eventHandlers.size(); i++) {
232                 intEventHandler = eventHandlers.get(i);
233                 if (intEventHandler.getUID().equals(eventHandler.getUID())) {
234                     index = i;
235                 } else {
236                     tempSubsList.addAll(intEventHandler.getSupportedEvents());
237                 }
238             }
239             if (index != -1) {
240                 intEventHandler = eventHandlers.remove(index);
241                 for (String eventName : intEventHandler.getSupportedEvents()) {
242                     if (!tempSubsList.contains(eventName)) {
243                         subscribedEvents.remove(eventName);
244                         subscribedEventsChanged = true;
245                     }
246                 }
247             }
248             if (subscribedEventsChanged) {
249                 // Because of the json-call unsubscribe?eventName=XY&subscriptionID=Z doesn't work like it is explained
250                 // in the dS-JSON-API, the whole EventListener will be restarted. The problem is, that not only the
251                 // given eventName, rather all events of the subscitionID will be deleted.
252                 restartListener();
253             }
254         }
255     }
256
257     /**
258      * Removes a subscribed event and unsubscibe it, if it is not needed by other {@link EventHandler}'s.
259      *
260      * @param unsubscribeEvent event name to unsubscibe
261      * @param eventHandlerID EventHandler-ID of the EventHandler that unsubscibe an event
262      */
263     public void removeSubscribe(String unsubscribeEvent, String eventHandlerID) {
264         if (subscribedEvents != null && !subscribedEvents.contains(unsubscribeEvent)) {
265             boolean eventNeededByAnotherHandler = false;
266             for (EventHandler handler : eventHandlers) {
267                 if (!handler.getUID().equals(eventHandlerID)) {
268                     eventNeededByAnotherHandler = handler.getSupportedEvents().contains(unsubscribeEvent);
269                 }
270             }
271             if (!eventNeededByAnotherHandler) {
272                 logger.debug("unsubscribeEvent: {} is not needed by other EventHandler's... unsubscribe it",
273                         unsubscribeEvent);
274                 subscribedEvents.remove(unsubscribeEvent);
275                 restartListener();
276             } else {
277                 logger.debug("unsubscribeEvent: {} is needed by other EventHandler's... dosen't unsubscribe it",
278                         unsubscribeEvent);
279             }
280         }
281     }
282
283     private void restartListener() {
284         internalStop();
285         if (!eventHandlers.isEmpty() && isStarted) {
286             logger.debug("Min one subscribed events was deleted, EventListener will be restarted");
287             internalStart();
288         }
289     }
290
291     /**
292      * Adds an event and subscribes it, if it is not subscribed already.
293      *
294      * @param subscribeEvent event name to subscribe
295      */
296     public void addSubscribe(String subscribeEvent) {
297         if (!subscribedEvents.contains(subscribeEvent)) {
298             subscribedEvents.add(subscribeEvent);
299             logger.debug("subscibeEvent: {} added", subscribeEvent);
300             if (subscribed) {
301                 subscribe(subscribeEvent);
302             }
303         }
304     }
305
306     /**
307      * Adds the events of the {@link List} and subscribe them, if an event is not subscribed already.
308      *
309      * @param subscribeEvents event name to subscribe
310      */
311     public void addSubscribeEvents(List<String> subscribeEvents) {
312         for (String eventName : subscribeEvents) {
313             subscribe(eventName);
314         }
315     }
316
317     private void getSubscriptionID() {
318         boolean subscriptionIDavailable = false;
319         while (!subscriptionIDavailable) {
320             String response = connManager.getDigitalSTROMAPI().getEvent(connManager.getSessionToken(), subscriptionID,
321                     timeout);
322
323             JsonObject responseObj = JSONResponseHandler.toJsonObject(response);
324
325             if (JSONResponseHandler.checkResponse(responseObj)) {
326                 subscriptionID++;
327             } else {
328                 String errorStr = null;
329                 if (responseObj != null && responseObj.get(JSONApiResponseKeysEnum.MESSAGE.getKey()) != null) {
330                     errorStr = responseObj.get(JSONApiResponseKeysEnum.MESSAGE.getKey()).getAsString();
331                 }
332                 if (errorStr != null && errorStr.contains(TOKEN_NOT_FOUND)) {
333                     subscriptionIDavailable = true;
334                 }
335             }
336         }
337     }
338
339     private boolean subscribe(String eventName) {
340         if (!subscribed) {
341             getSubscriptionID();
342         }
343         subscribed = connManager.getDigitalSTROMAPI().subscribeEvent(connManager.getSessionToken(), eventName,
344                 subscriptionID, config.getConnectionTimeout(), config.getReadTimeout());
345
346         if (subscribed) {
347             logger.debug("subscribed event: {} to subscriptionID: {}", eventName, subscriptionID);
348         } else {
349             logger.error(
350                     "Couldn't subscribe event {} ... maybe timeout because system is too busy ... event will be tried to subscribe later again ... ",
351                     eventName);
352         }
353         return subscribed;
354     }
355
356     private void subscribe(final List<String> eventNames) {
357         subscriptionScheduler = scheduler.scheduleWithFixedDelay(() -> {
358             eventNames.forEach(this::subscribe);
359             stopSubscriptionScheduler();
360         }, 0, SUBSCRIBE_DELAY, TimeUnit.MILLISECONDS);
361     }
362
363     private final Runnable runableListener = new Runnable() {
364
365         @Override
366         public void run() {
367             if (subscribed) {
368                 String response = connManager.getDigitalSTROMAPI().getEvent(connManager.getSessionToken(),
369                         subscriptionID, timeout);
370                 JsonObject responseObj = JSONResponseHandler.toJsonObject(response);
371
372                 if (JSONResponseHandler.checkResponse(responseObj)) {
373                     JsonObject obj = JSONResponseHandler.getResultJsonObject(responseObj);
374                     if (obj != null && obj.get(JSONApiResponseKeysEnum.EVENTS.getKey()).isJsonArray()) {
375                         JsonArray array = obj.get(JSONApiResponseKeysEnum.EVENTS.getKey()).getAsJsonArray();
376                         handleEvent(array);
377                     }
378                 } else {
379                     String errorStr = null;
380                     if (responseObj != null && responseObj.get(JSONApiResponseKeysEnum.MESSAGE.getKey()) != null) {
381                         errorStr = responseObj.get(JSONApiResponseKeysEnum.MESSAGE.getKey()).getAsString();
382                     }
383                     if (errorStr != null && (errorStr.equals(INVALID_SESSION) || errorStr.contains(TOKEN_NOT_FOUND))) {
384                         unsubscribe();
385                         subscribe(subscribedEvents);
386                     } else if (errorStr != null) {
387                         pollingScheduler.cancel(true);
388                         logger.error("Unknown error message at event response: {}", errorStr);
389                     }
390                 }
391             } else {
392                 subscribe(subscribedEvents);
393             }
394         }
395     };
396
397     private void unsubscribe() {
398         for (String eventName : this.subscribedEvents) {
399             connManager.getDigitalSTROMAPI().unsubscribeEvent(this.connManager.getSessionToken(), eventName,
400                     this.subscriptionID, config.getConnectionTimeout(), config.getReadTimeout());
401         }
402     }
403
404     private void handleEvent(JsonArray array) {
405         if (array.size() > 0) {
406             Event event = new JSONEventImpl(array);
407             for (EventItem item : event.getEventItems()) {
408                 logger.debug("detect event {}", item.toString());
409                 for (EventHandler handler : eventHandlers) {
410                     if (handler.supportsEvent(item.getName())) {
411                         logger.debug("inform handler with id {} about event {}", handler.getUID(), item.toString());
412                         handler.handleEvent(item);
413                     }
414                 }
415             }
416         }
417     }
418 }