2 * Copyright (c) 2010-2020 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.insteon.internal.driver;
16 import java.util.Iterator;
17 import java.util.SortedSet;
18 import java.util.TreeSet;
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;
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.
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.
40 * @author Bernd Pfrommer - Initial contribution
41 * @author Rob Nielsen - Port to openHAB 2 insteon binding
44 @SuppressWarnings("null")
46 private static final long MIN_MSEC_BETWEEN_POLLS = 2000L;
48 private final Logger logger = LoggerFactory.getLogger(Poller.class);
49 private static Poller poller = new Poller(); // for singleton
51 private @Nullable Thread pollThread = null;
52 private TreeSet<PQEntry> pollQueue = new TreeSet<>();
53 private boolean keepRunning = true;
62 * Get size of poll queue
64 * @return number of devices being polled
66 public int getSizeOfQueue() {
67 return (pollQueue.size());
71 * Register a device for polling.
73 * @param d device to register for polling
74 * @param aNumDev approximate number of total devices
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
81 int n = pollQueue.size();
82 long pollDelay = n * d.getPollInterval() / (aNumDev > 0 ? aNumDev : 1);
83 addToPollQueue(d, System.currentTimeMillis() + pollDelay);
89 * Start polling a given device
91 * @param d reference to the device to be polled
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())) {
98 logger.debug("stopped polling device {}", d);
105 * Starts the poller thread
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);
117 * Stops the poller thread
120 logger.debug("stopping poller!");
121 synchronized (pollQueue) {
127 if (pollThread != null) {
132 } catch (InterruptedException e) {
133 logger.debug("got interrupted on exit: {}", e.getMessage());
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.
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.
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)));
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
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
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));
168 // all entries in the poll queue are ahead of the new element,
169 // go ahead and simply add it to the end
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
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) {
186 logger.trace("dev {} time {} found slot between {} and {}", d, aTime, tprev, tcurr);
191 expTime = prev.getExpirationTime() + MIN_MSEC_BETWEEN_POLLS;
198 private class PollQueueReader implements Runnable {
201 logger.debug("starting poll thread.");
202 synchronized (pollQueue) {
203 while (keepRunning) {
206 } catch (InterruptedException e) {
207 logger.warn("poll queue reader thread interrupted!");
212 logger.debug("poll thread exiting");
216 * Waits for first element of poll queue to become current,
219 * @throws InterruptedException
221 private void readPollQueue() throws InterruptedException {
222 while (pollQueue.isEmpty() && keepRunning) {
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);
236 } else { // queue entry has expired, process it!
237 logger.trace("entry {} expired at time {}", pqe, now);
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.
246 * @param now the current time
248 private void processQueue(long now) {
249 PQEntry pqe = pollQueue.pollFirst();
250 pqe.getDevice().doPoll(0);
251 addToPollQueue(pqe.getDevice(), now + pqe.getDevice().getPollInterval());
256 * A poll queue entry corresponds to a single device that needs
259 * @author Bernd Pfrommer - Initial contribution
263 private static class PQEntry implements Comparable<PQEntry> {
264 private InsteonDevice dev;
265 private long expirationTime;
267 PQEntry(InsteonDevice dev, long time) {
269 this.expirationTime = time;
272 long getExpirationTime() {
273 return expirationTime;
276 InsteonDevice getDevice() {
281 public int compareTo(PQEntry b) {
282 return (int) (expirationTime - b.expirationTime);
286 public String toString() {
287 return dev.getAddress().toString() + "/" + String.format("%tc", new Date(expirationTime));
292 * Singleton pattern instance() method
294 * @return the poller instance
296 public static synchronized Poller instance() {