]> git.basschouten.com Git - openhab-addons.git/blob
e675f1c834e49e7c448277e7f62d512bddbb4c0e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.insteon.internal.driver;
14
15 import java.sql.Date;
16 import java.util.Iterator;
17 import java.util.SortedSet;
18 import java.util.TreeSet;
19
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.insteon.internal.device.InsteonDevice;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
25
26 /**
27  * This class manages the polling of all devices.
28  * Between successive polls of a any device there is a quiet time of
29  * at least MIN_MSEC_BETWEEN_POLLS. This avoids bunching up of poll messages
30  * and keeps the network bandwidth open for other messages.
31  *
32  * - An entry in the poll queue corresponds to a single device, i.e. each device should
33  * have exactly one entry in the poll queue. That entry is created when startPolling()
34  * is called, and then re-enqueued whenever it expires.
35  * - When a device comes up for polling, its doPoll() method is called, which in turn
36  * puts an entry into that devices request queue. So the Poller class actually never
37  * sends out messages directly. That is done by the device itself via its request
38  * queue. The poller just reminds the device to poll.
39  *
40  * @author Bernd Pfrommer - Initial contribution
41  * @author Rob Nielsen - Port to openHAB 2 insteon binding
42  */
43 @NonNullByDefault
44 @SuppressWarnings("null")
45 public class Poller {
46     private static final long MIN_MSEC_BETWEEN_POLLS = 2000L;
47
48     private final Logger logger = LoggerFactory.getLogger(Poller.class);
49     private static Poller poller = new Poller(); // for singleton
50
51     private @Nullable Thread pollThread = null;
52     private TreeSet<PQEntry> pollQueue = new TreeSet<>();
53     private boolean keepRunning = true;
54
55     /**
56      * Constructor
57      */
58     private Poller() {
59     }
60
61     /**
62      * Get size of poll queue
63      *
64      * @return number of devices being polled
65      */
66     public int getSizeOfQueue() {
67         return (pollQueue.size());
68     }
69
70     /**
71      * Register a device for polling.
72      *
73      * @param d device to register for polling
74      * @param aNumDev approximate number of total devices
75      */
76     public void startPolling(InsteonDevice d, int aNumDev) {
77         logger.debug("start polling device {}", d);
78         synchronized (pollQueue) {
79             // try to spread out the scheduling when
80             // starting up
81             int n = pollQueue.size();
82             long pollDelay = n * d.getPollInterval() / (aNumDev > 0 ? aNumDev : 1);
83             addToPollQueue(d, System.currentTimeMillis() + pollDelay);
84             pollQueue.notify();
85         }
86     }
87
88     /**
89      * Start polling a given device
90      *
91      * @param d reference to the device to be polled
92      */
93     public void stopPolling(InsteonDevice d) {
94         synchronized (pollQueue) {
95             for (Iterator<PQEntry> i = pollQueue.iterator(); i.hasNext();) {
96                 if (i.next().getDevice().getAddress().equals(d.getAddress())) {
97                     i.remove();
98                     logger.debug("stopped polling device {}", d);
99                 }
100             }
101         }
102     }
103
104     /**
105      * Starts the poller thread
106      */
107     public void start() {
108         if (pollThread == null) {
109             pollThread = new Thread(new PollQueueReader());
110             pollThread.setName("Insteon Poll Queue Reader");
111             pollThread.setDaemon(true);
112             pollThread.start();
113         }
114     }
115
116     /**
117      * Stops the poller thread
118      */
119     public void stop() {
120         logger.debug("stopping poller!");
121         synchronized (pollQueue) {
122             pollQueue.clear();
123             keepRunning = false;
124             pollQueue.notify();
125         }
126         try {
127             if (pollThread != null) {
128                 pollThread.join();
129                 pollThread = null;
130             }
131             keepRunning = true;
132         } catch (InterruptedException e) {
133             logger.debug("got interrupted on exit: {}", e.getMessage());
134         }
135     }
136
137     /**
138      * Adds a device to the poll queue. After this call, the device's doPoll() method
139      * will be called according to the polling frequency set.
140      *
141      * @param d the device to poll periodically
142      * @param time the target time for the next poll to happen. Note that this time is merely
143      *            a suggestion, and may be adjusted, because there must be at least a minimum gap in polling.
144      */
145
146     private void addToPollQueue(InsteonDevice d, long time) {
147         long texp = findNextExpirationTime(d, time);
148         PQEntry ne = new PQEntry(d, texp);
149         logger.trace("added entry {} originally aimed at time {}", ne, String.format("%tc", new Date(time)));
150         pollQueue.add(ne);
151     }
152
153     /**
154      * Finds the best expiration time for a poll queue, i.e. a time slot that is after the
155      * desired expiration time, but does not collide with any of the already scheduled
156      * polls.
157      *
158      * @param d device to poll (for logging)
159      * @param aTime desired time after which the device should be polled
160      * @return the suggested time to poll
161      */
162
163     private long findNextExpirationTime(InsteonDevice d, long aTime) {
164         long expTime = aTime;
165         // tailSet finds all those that expire after aTime - buffer
166         SortedSet<PQEntry> ts = pollQueue.tailSet(new PQEntry(d, aTime - MIN_MSEC_BETWEEN_POLLS));
167         if (ts.isEmpty()) {
168             // all entries in the poll queue are ahead of the new element,
169             // go ahead and simply add it to the end
170             expTime = aTime;
171         } else {
172             Iterator<PQEntry> pqi = ts.iterator();
173             PQEntry prev = pqi.next();
174             if (prev.getExpirationTime() > aTime + MIN_MSEC_BETWEEN_POLLS) {
175                 // there is a time slot free before the head of the tail set
176                 expTime = aTime;
177             } else {
178                 // look for a gap where we can squeeze in
179                 // a new poll while maintaining MIN_MSEC_BETWEEN_POLLS
180                 while (pqi.hasNext()) {
181                     PQEntry pqe = pqi.next();
182                     long tcurr = pqe.getExpirationTime();
183                     long tprev = prev.getExpirationTime();
184                     if (tcurr - tprev >= 2 * MIN_MSEC_BETWEEN_POLLS) {
185                         // found gap
186                         logger.trace("dev {} time {} found slot between {} and {}", d, aTime, tprev, tcurr);
187                         break;
188                     }
189                     prev = pqe;
190                 }
191                 expTime = prev.getExpirationTime() + MIN_MSEC_BETWEEN_POLLS;
192             }
193         }
194         return expTime;
195     }
196
197     @NonNullByDefault
198     private class PollQueueReader implements Runnable {
199         @Override
200         public void run() {
201             logger.debug("starting poll thread.");
202             synchronized (pollQueue) {
203                 while (keepRunning) {
204                     try {
205                         readPollQueue();
206                     } catch (InterruptedException e) {
207                         logger.warn("poll queue reader thread interrupted!");
208                         break;
209                     }
210                 }
211             }
212             logger.debug("poll thread exiting");
213         }
214
215         /**
216          * Waits for first element of poll queue to become current,
217          * then process it.
218          *
219          * @throws InterruptedException
220          */
221         private void readPollQueue() throws InterruptedException {
222             while (pollQueue.isEmpty() && keepRunning) {
223                 pollQueue.wait();
224             }
225             if (!keepRunning) {
226                 return;
227             }
228             // something is in the queue
229             long now = System.currentTimeMillis();
230             PQEntry pqe = pollQueue.first();
231             long tfirst = pqe.getExpirationTime();
232             long dt = tfirst - now;
233             if (dt > 0) { // must wait for this item to expire
234                 logger.trace("waiting for {} msec until {} comes due", dt, pqe);
235                 pollQueue.wait(dt);
236             } else { // queue entry has expired, process it!
237                 logger.trace("entry {} expired at time {}", pqe, now);
238                 processQueue(now);
239             }
240         }
241
242         /**
243          * Takes first element off the poll queue, polls the corresponding device,
244          * and puts the device back into the poll queue to be polled again later.
245          *
246          * @param now the current time
247          */
248         private void processQueue(long now) {
249             PQEntry pqe = pollQueue.pollFirst();
250             pqe.getDevice().doPoll(0);
251             addToPollQueue(pqe.getDevice(), now + pqe.getDevice().getPollInterval());
252         }
253     }
254
255     /**
256      * A poll queue entry corresponds to a single device that needs
257      * to be polled.
258      *
259      * @author Bernd Pfrommer - Initial contribution
260      *
261      */
262     @NonNullByDefault
263     private static class PQEntry implements Comparable<PQEntry> {
264         private InsteonDevice dev;
265         private long expirationTime;
266
267         PQEntry(InsteonDevice dev, long time) {
268             this.dev = dev;
269             this.expirationTime = time;
270         }
271
272         long getExpirationTime() {
273             return expirationTime;
274         }
275
276         InsteonDevice getDevice() {
277             return dev;
278         }
279
280         @Override
281         public int compareTo(PQEntry b) {
282             return (int) (expirationTime - b.expirationTime);
283         }
284
285         @Override
286         public String toString() {
287             return dev.getAddress().toString() + "/" + String.format("%tc", new Date(expirationTime));
288         }
289     }
290
291     /**
292      * Singleton pattern instance() method
293      *
294      * @return the poller instance
295      */
296     public static synchronized Poller instance() {
297         poller.start();
298         return (poller);
299     }
300 }