/**
* 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.knx.internal.bus;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.openhab.binding.knx.internal.connection.KNXConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tuwien.auto.calimero.datapoint.Datapoint;
/**
* This is the central class that takes care of the refreshing (cyclical reading) of GAs from the KNX bus.
*
* @author Volker Daube
* @since 1.6.0
*
*/
public class KNXBusReaderScheduler {
private static final Logger sLogger = LoggerFactory.getLogger(KNXBusReaderScheduler.class);
private final BlockingQueue<Datapoint> mReadQueue = new LinkedBlockingQueue<Datapoint>();
private static Map<Integer, List<Datapoint>> mScheduleMap = new ConcurrentHashMap<Integer, List<Datapoint>>();
private ScheduledExecutorService mScheduledExecutorService;
private boolean mIsRunning = false;
/** the datapoint initializer, which runs in a separate thread */
private KNXBindingDatapointReaderTask mDatapointReaderTask = null;
/**
* Starts the scheduler
*/
public void start() {
sLogger.trace("Starting auto refresh scheduler");
sLogger.debug("Starting reader task.");
mDatapointReaderTask = new KNXBindingDatapointReaderTask(mReadQueue);
mDatapointReaderTask.start();
sLogger.debug("Starting schedule executor.");
mScheduledExecutorService = Executors.newScheduledThreadPool(KNXConnection.getNumberOfThreads());
mIsRunning = true;
}
/**
* Stop the scheduler
*/
public void stop() {
sLogger.trace("Stopping auto refresh scheduler");
sLogger.trace("Clearing all items from the refresher queue");
mReadQueue.clear();
sLogger.debug("Terminating schedule executor.");
mScheduledExecutorService.shutdown();
try {
if (mScheduledExecutorService.awaitTermination(KNXConnection.getScheduledExecutorServiceShutdownTimeout(),
TimeUnit.SECONDS)) {
sLogger.debug("Auto refresh scheduler successfully terminated");
} else {
sLogger.debug("Auto refresh scheduler couldn't be terminated and termination timed out.");
}
} catch (InterruptedException e) {
sLogger.warn("Auto refresh scheduler: interrupted while waiting for termination.");
Thread.currentThread().interrupt();
}
sLogger.trace("Stopping reader task");
mDatapointReaderTask.interrupt();
mIsRunning = false;
}
public boolean isRunning() {
return mIsRunning;
}
/**
* Clears all datapoints from the scheduler
*/
public synchronized void clear() {
sLogger.trace("Clearing all datapoints from auto refresh scheduler");
mReadQueue.clear();
// Restarting schedule executor
if (mScheduledExecutorService != null) {
sLogger.debug("Schedule executor restart.");
mScheduledExecutorService.shutdown();
try {
if (mScheduledExecutorService.awaitTermination(
KNXConnection.getScheduledExecutorServiceShutdownTimeout(), TimeUnit.SECONDS)) {
sLogger.debug("Schedule executor restart: successfully terminated old instance");
} else {
sLogger.debug("Schedule executor restart failed: termination timed out.");
}
} catch (InterruptedException e) {
sLogger.debug("Schedule executor restart failed: interrupted while waiting for termination.");
Thread.currentThread().interrupt();
}
mScheduledExecutorService = Executors.newScheduledThreadPool(KNXConnection.getNumberOfThreads());
sLogger.debug("Schedule executor restart: started.");
}
for (Iterator<Integer> iterator = mScheduleMap.keySet().iterator(); iterator.hasNext();) {
int autoRefreshTimeInSecs = iterator.next();
List<Datapoint> dpList = mScheduleMap.get(autoRefreshTimeInSecs);
synchronized (dpList) {
sLogger.debug("Clearing list {}", autoRefreshTimeInSecs);
dpList.clear();
}
sLogger.debug("Removing list {} from scheduler", autoRefreshTimeInSecs);
iterator.remove();
}
}
/**
* Schedules immediate and one-time reading of a <code>Datapoint</code>.
*
* @param datapoint the <code>Datapoint</code> to read
* @return false if the datapoint is null.
*/
public synchronized boolean readOnce(Datapoint datapoint) {
if (datapoint == null) {
sLogger.error("Argument datapoint cannot be null");
return false;
}
if (mReadQueue.size() > KNXConnection.getMaxRefreshQueueEntries()) {
sLogger.error("Maximum number of permissible reading queue entries reached ('{}'). Ignoring new entries.",
KNXConnection.getMaxRefreshQueueEntries());
return false;
}
sLogger.debug("Datapoint '{}': one time reading scheduled.", datapoint.getName());
return mReadQueue.add(datapoint);
}
/**
* Schedules a <code>Datapoint</code> to be cyclicly read. When parameter
* <code>autoRefreshTimeInSecs</code> is 0 then calling ths method is equal
* to calling <link>readOnce</link>. This function will return true if the <code>Datapoint</code>
* was added or if it was already scheduled with an identical <code>autoRefreshTimeInSecs</code>.
*
* @param datapoint
* the <code>Datapoint</code> to be read
* @param autoRefreshTimeInSecs
* time in seconds specifying the reading cycle. 0 is equal to
* calling <link>readOnce</link>
* @return true if the Datapoint was scheduled for reading, false in all
* other cases
*/
public synchronized boolean scheduleRead(Datapoint datapoint, int autoRefreshTimeInSecs) {
if (datapoint == null) {
sLogger.error("Argument datapoint cannot be null");
return false;
}
if (autoRefreshTimeInSecs < 0) {
sLogger.error("AutoRefreshTimeInSecs must be >= 0 for datapoint '{}'", datapoint.getName());
return false;
}
if (autoRefreshTimeInSecs == 0) {
return readOnce(datapoint);
}
if (mReadQueue.size() > KNXConnection.getMaxRefreshQueueEntries()) {
sLogger.error("Maximum number of permissible reading queue entries reached ('{}'). Ignoring new entries.",
KNXConnection.getMaxRefreshQueueEntries());
return false;
}
// Check if datapoint is already present in another list and if so, remove it
int oldListNumber = getAutoRefreshTimeInSecs(datapoint);
if (oldListNumber > 0) {
if (oldListNumber == autoRefreshTimeInSecs) {
sLogger.debug("Datapoint '{}' was already in auto refresh list {}", datapoint.getName(),
autoRefreshTimeInSecs);
return true;
}
List<Datapoint> oldList = mScheduleMap.get(oldListNumber);
synchronized (oldList) {
sLogger.debug("Datapoint '{}' already present in different list: {}, removing", datapoint.getName(),
oldListNumber);
/*
* The simple method to remove a <code>Datapoint</code> from a
* list would be <code>dpList.remove(datapoint)</code>
* Unfortunately, this cannot be used as the
* <code>Datapoint.equals()</code> method is comparing objects
* and sometimes new objects are being created for example when
* a configuration file is reread.
*/
for (Iterator<Datapoint> iterator = oldList.iterator(); iterator.hasNext();) {
Datapoint dp = iterator.next();
if (dp.toString().equals(datapoint.toString())) {
iterator.remove();
}
}
}
}
// Check if we have a list for autoRefreshTimeInSecs. If not create it.
if (!mScheduleMap.containsKey(autoRefreshTimeInSecs)) {
sLogger.debug("Creating auto refresh list: {}.", autoRefreshTimeInSecs);
mScheduleMap.put(autoRefreshTimeInSecs, new LinkedList<Datapoint>());
if (mIsRunning) {
// Start scheduled task for the new time
sLogger.debug("Starting auto refresh cycle {}", autoRefreshTimeInSecs);
mScheduledExecutorService.scheduleAtFixedRate(new AutoRefreshTask(autoRefreshTimeInSecs),
autoRefreshTimeInSecs, autoRefreshTimeInSecs, TimeUnit.SECONDS);
}
}
// Add the datapoint to the list
List<Datapoint> dpList = mScheduleMap.get(autoRefreshTimeInSecs);
synchronized (dpList) {
sLogger.debug("Adding datapoint '{}' to auto refresh list {}.", datapoint.getName(), autoRefreshTimeInSecs);
return dpList.add(datapoint);
}
}
/**
* Returns the auto refresh time in seconds for <code>datapoint</code>.
* If the datapoint is not present <code>0</code> is returned.
*
* @param datapoint the data point to check
* @return the auto refresh time in seconds if datapoint was added previously. <code>0</code> otherwise.
*/
private synchronized int getAutoRefreshTimeInSecs(Datapoint datapoint) {
for (int number : mScheduleMap.keySet()) {
List<Datapoint> dpList = mScheduleMap.get(number);
synchronized (dpList) {
/*
* The simple method to see if a <code>Datapoint</code> is
* already in the list would be
* <code>dpList.contains(datapoint)</code> Unfortunately, this
* cannot be used as the Datapoint.equals() method is comparing
* objects and sometimes new objects are being created for
* example when a configuration file is reread.
*/
for (Datapoint dp : dpList) {
if (dp.toString().equals(datapoint.toString())) {
return number;
}
}
}
}
return 0;
}
private final class AutoRefreshTask implements Runnable {
private int autoRefreshTimeInSecs = 0;
public AutoRefreshTask(int autoRefreshTimeInSecs) {
this.autoRefreshTimeInSecs = autoRefreshTimeInSecs;
}
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
if (KNXConnection.sShutdown) {
return;
}
List<Datapoint> dpList = null;
synchronized (mScheduleMap) {
dpList = mScheduleMap.get(autoRefreshTimeInSecs);
if (dpList == null) {
sLogger.debug("Autorefresh: List {} was deleted. Terminating thread.", autoRefreshTimeInSecs);
} else {
sLogger.debug("Autorefresh: Adding {} item(s) with refresh time {} to reader queue.", dpList.size(),
autoRefreshTimeInSecs);
synchronized (dpList) {
mReadQueue.addAll(dpList);
}
}
}
}
}
}