/**
* Copyright (c) 2010-2016 by the respective copyright holders.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.openhab.binding.insteonplm.internal.driver;
import java.sql.Date;
import java.util.Iterator;
import java.util.SortedSet;
import java.util.TreeSet;
import org.openhab.binding.insteonplm.internal.device.InsteonDevice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class manages the polling of all devices.
* Between successive polls of a any device there is a quiet time of
* at least MIN_MSEC_BETWEEN_POLLS. This avoids bunching up of poll messages
* and keeps the network bandwidth open for other messages.
*
* - An entry in the poll queue corresponds to a single device, i.e. each device should
* have exactly one entry in the poll queue. That entry is created when startPolling()
* is called, and then re-enqueued whenever it expires.
* - When a device comes up for polling, its doPoll() method is called, which in turn
* puts an entry into that devices request queue. So the Poller class actually never
* sends out messages directly. That is done by the device itself via its request
* queue. The poller just reminds the device to poll.
*
* @author Bernd Pfrommer
* @since 1.5.0
*/
public class Poller {
private static final Logger logger = LoggerFactory.getLogger(Poller.class);
private static Poller s_poller; // for singleton
private Thread m_pollThread = null;
private TreeSet<PQEntry> m_pollQueue = new TreeSet<PQEntry>();
private final long MIN_MSEC_BETWEEN_POLLS = 2000L;
private boolean m_keepRunning = true;
/**
* Constructor
*/
private Poller() {
}
/**
* Get size of poll queue
*
* @return number of devices being polled
*/
public int getSizeOfQueue() {
return (m_pollQueue.size());
}
/**
* Register a device for polling.
*
* @param d device to register for polling
* @param aNumDev approximate number of total devices
*/
public void startPolling(InsteonDevice d, int aNumDev) {
logger.debug("start polling device {}", d);
synchronized (m_pollQueue) {
// try to spread out the scheduling when
// starting up
int n = m_pollQueue.size();
long pollDelay = n * d.getPollInterval() / (aNumDev > 0 ? aNumDev : 1);
addToPollQueue(d, System.currentTimeMillis() + pollDelay);
m_pollQueue.notify();
}
}
/**
* Start polling a given device
*
* @param d reference to the device to be polled
*/
public void stopPolling(InsteonDevice d) {
synchronized (m_pollQueue) {
for (Iterator<PQEntry> i = m_pollQueue.iterator(); i.hasNext();) {
if (i.next().getDevice().getAddress().equals(d.getAddress())) {
i.remove();
logger.debug("stopped polling device {}", d);
}
}
}
}
/**
* Starts the poller thread
*/
public void start() {
if (m_pollThread == null) {
m_pollThread = new Thread(new PollQueueReader());
m_pollThread.start();
}
}
/**
* Stops the poller thread
*/
public void stop() {
logger.debug("stopping poller!");
synchronized (m_pollQueue) {
m_pollQueue.clear();
m_keepRunning = false;
m_pollQueue.notify();
}
try {
m_pollThread.join();
m_keepRunning = true;
m_pollThread = null;
} catch (InterruptedException e) {
logger.debug("got interrupted on exit: {}", e.getMessage());
}
}
/**
* Adds a device to the poll queue. After this call, the device's doPoll() method
* will be called according to the polling frequency set.
*
* @param d the device to poll periodically
* @param time the target time for the next poll to happen. Note that this time is merely
* a suggestion, and may be adjusted, because there must be at least a minimum gap in polling.
*/
private void addToPollQueue(InsteonDevice d, long time) {
long texp = findNextExpirationTime(d, time);
PQEntry ne = new PQEntry(d, texp);
logger.trace("added entry {} originally aimed at time {}", ne, String.format("%tc", new Date(time)));
m_pollQueue.add(ne);
}
/**
* Finds the best expiration time for a poll queue, i.e. a time slot that is after the
* desired expiration time, but does not collide with any of the already scheduled
* polls.
*
* @param d device to poll (for logging)
* @param aTime desired time after which the device should be polled
* @return the suggested time to poll
*/
private long findNextExpirationTime(InsteonDevice d, long aTime) {
long expTime = aTime;
// tailSet finds all those that expire after aTime - buffer
SortedSet<PQEntry> ts = m_pollQueue.tailSet(new PQEntry(d, aTime - MIN_MSEC_BETWEEN_POLLS));
if (ts.isEmpty()) {
// all entries in the poll queue are ahead of the new element,
// go ahead and simply add it to the end
expTime = aTime;
} else {
Iterator<PQEntry> pqi = ts.iterator();
PQEntry prev = pqi.next();
if (prev.getExpirationTime() > aTime + MIN_MSEC_BETWEEN_POLLS) {
// there is a time slot free before the head of the tail set
expTime = aTime;
} else {
// look for a gap where we can squeeze in
// a new poll while maintaining MIN_MSEC_BETWEEN_POLLS
while (pqi.hasNext()) {
PQEntry pqe = pqi.next();
long tcurr = pqe.getExpirationTime();
long tprev = prev.getExpirationTime();
if (tcurr - tprev >= 2 * MIN_MSEC_BETWEEN_POLLS) {
// found gap
logger.trace("dev {} time {} found slot between {} and {}", d, aTime, tprev, tcurr);
break;
}
prev = pqe;
}
expTime = prev.getExpirationTime() + MIN_MSEC_BETWEEN_POLLS;
}
}
return expTime;
}
private class PollQueueReader implements Runnable {
@Override
public void run() {
logger.debug("starting poll thread.");
synchronized (m_pollQueue) {
while (m_keepRunning) {
try {
readPollQueue();
} catch (InterruptedException e) {
logger.warn("poll queue reader thread interrupted!");
break;
}
}
}
logger.debug("poll thread exiting");
}
/**
* Waits for first element of poll queue to become current,
* then process it.
*
* @throws InterruptedException
*/
private void readPollQueue() throws InterruptedException {
while (m_pollQueue.isEmpty() && m_keepRunning) {
m_pollQueue.wait();
}
if (!m_keepRunning) {
return;
}
// something is in the queue
long now = System.currentTimeMillis();
PQEntry pqe = m_pollQueue.first();
long tfirst = pqe.getExpirationTime();
long dt = tfirst - now;
if (dt > 0) { // must wait for this item to expire
logger.trace("waiting for {} msec until {} comes due", dt, pqe);
m_pollQueue.wait(dt);
} else { // queue entry has expired, process it!
logger.trace("entry {} expired at time {}", pqe, now);
processQueue(now);
}
}
/**
* Takes first element off the poll queue, polls the corresponding device,
* and puts the device back into the poll queue to be polled again later.
*
* @param now the current time
*/
private void processQueue(long now) {
PQEntry pqe = m_pollQueue.pollFirst();
pqe.getDevice().doPoll(0);
addToPollQueue(pqe.getDevice(), now + pqe.getDevice().getPollInterval());
}
}
/**
* A poll queue entry corresponds to a single device that needs
* to be polled.
*
* @author Bernd Pfrommer
*
*/
private static class PQEntry implements Comparable<PQEntry> {
private InsteonDevice m_dev = null;
private long m_expirationTime = 0L;
PQEntry(InsteonDevice dev, long time) {
m_dev = dev;
m_expirationTime = time;
}
long getExpirationTime() {
return m_expirationTime;
}
InsteonDevice getDevice() {
return m_dev;
}
@Override
public int compareTo(PQEntry b) {
return (int) (m_expirationTime - b.m_expirationTime);
}
@Override
public String toString() {
return m_dev.getAddress().toString() + "/" + String.format("%tc", new Date(m_expirationTime));
}
}
/**
* Singleton pattern instance() method
*
* @return the poller instance
*/
public static synchronized Poller s_instance() {
if (s_poller == null) {
s_poller = new Poller();
}
s_poller.start();
return (s_poller);
}
}