Using UTC instead of LOCAL time.

Former-commit-id: 21831d97d7f616313e0e90bf8ae69e04dea57685
This commit is contained in:
Daniel Collin 2015-12-18 10:00:27 +01:00
parent 4d9589dc9a
commit 5036ec90e9
6 changed files with 461 additions and 382 deletions

View file

@ -1,116 +1,117 @@
package se.koc.hal;
import zutil.db.DBConnection;
import zutil.db.DBUpgradeHandler;
import zutil.db.handler.PropertiesSQLResult;
import zutil.io.file.FileUtil;
import zutil.log.LogUtil;
import java.io.File;
import java.io.FileReader;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Properties;
import java.util.logging.Logger;
public class HalContext {
private static final Logger logger = LogUtil.getLogger();
// Constants
private static final String PROPERTY_DB_VERSION = "db_version";
private static final String CONF_FILE = "hal.conf";
private static final String DB_FILE = "hal.db";
private static final String DEFAULT_DB_FILE = "hal-default.db";
// Variables
private static DBConnection db;
private static Properties defaultFileConf;
private static Properties fileConf;
private static Properties dbConf;
static {
defaultFileConf = new Properties();
defaultFileConf.setProperty("http_port", ""+8080);
defaultFileConf.setProperty("sync_port", ""+6666);
}
public static void initialize(){
try {
// Read conf
fileConf = new Properties(defaultFileConf);
FileReader in = new FileReader(CONF_FILE);
fileConf.load(in);
in.close();
// Init DB
File dbFile = FileUtil.find(DB_FILE);
if(dbFile == null){
logger.info("Creating new DB...");
FileUtil.copy(dbFile, FileUtil.find(DEFAULT_DB_FILE));
}
db = new DBConnection(DBConnection.DBMS.SQLite, DB_FILE);
// Read DB conf
dbConf = db.exec("SELECT * FROM conf", new PropertiesSQLResult());
// Upgrade DB needed?
DBConnection referenceDB = new DBConnection(DBConnection.DBMS.SQLite, DEFAULT_DB_FILE);
Properties defaultDBConf =
referenceDB.exec("SELECT * FROM conf", new PropertiesSQLResult());
// Check DB version
logger.fine("DB version: "+ dbConf.getProperty(PROPERTY_DB_VERSION));
if(dbConf.getProperty(PROPERTY_DB_VERSION) == null ||
defaultDBConf.getProperty(PROPERTY_DB_VERSION).compareTo(dbConf.getProperty(PROPERTY_DB_VERSION)) > 0) {
logger.info("Starting DB upgrade...");
File backupDB = FileUtil.getNextFile(dbFile);
logger.fine("Backing up DB to: "+ backupDB);
FileUtil.copy(dbFile, backupDB);
logger.fine(String.format("Upgrading DB (from: v%s, to: v%s)...",
dbConf.getProperty(PROPERTY_DB_VERSION),
defaultDBConf.getProperty(PROPERTY_DB_VERSION)));
DBUpgradeHandler handler = new DBUpgradeHandler(referenceDB);
handler.setTargetDB(db);
//handler.setForcedDBUpgrade(true);
handler.upgrade();
logger.info("DB upgrade done");
dbConf.setProperty(PROPERTY_DB_VERSION, defaultDBConf.getProperty(PROPERTY_DB_VERSION));
storeProperties();
}
referenceDB.close();
} catch (Exception e){
throw new RuntimeException(e);
}
}
public static String getStringProperty(String key){
String value = fileConf.getProperty(key);
if(value == null)
value = dbConf.getProperty(key);
return value;
}
public static int getIntegerProperty(String key){
return Integer.parseInt(getStringProperty(key));
}
public synchronized static void storeProperties() throws SQLException {
logger.fine("Saving conf to DB...");
PreparedStatement stmt = db.getPreparedStatement("REPLACE INTO conf (key, value) VALUES (?, ?)");
for(Object key : dbConf.keySet()){
stmt.setObject(1, key);
stmt.setObject(2, dbConf.get(key));
stmt.addBatch();
}
DBConnection.execBatch(stmt);
}
public static DBConnection getDB(){
return db;
}
}
package se.koc.hal;
import zutil.db.DBConnection;
import zutil.db.DBUpgradeHandler;
import zutil.db.handler.PropertiesSQLResult;
import zutil.io.file.FileUtil;
import zutil.log.LogUtil;
import java.io.File;
import java.io.FileReader;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Properties;
import java.util.logging.Logger;
public class HalContext {
private static final Logger logger = LogUtil.getLogger();
// Constants
private static final String PROPERTY_DB_VERSION = "db_version";
private static final String CONF_FILE = "hal.conf";
private static final String DB_FILE = "hal.db";
private static final String DEFAULT_DB_FILE = "hal-default.db";
// Variables
private static DBConnection db;
private static Properties defaultFileConf;
private static Properties fileConf;
private static Properties dbConf;
static {
defaultFileConf = new Properties();
defaultFileConf.setProperty("http_port", ""+8080);
defaultFileConf.setProperty("sync_port", ""+6666);
}
public static void initialize(){
try {
// Read conf
fileConf = new Properties(defaultFileConf);
FileReader in = new FileReader(CONF_FILE);
fileConf.load(in);
in.close();
// Init DB
File dbFile = FileUtil.find(DB_FILE);
if(dbFile == null){
logger.info("Creating new DB...");
FileUtil.copy(dbFile, FileUtil.find(DEFAULT_DB_FILE));
}
db = new DBConnection(DBConnection.DBMS.SQLite, DB_FILE);
// Read DB conf
dbConf = db.exec("SELECT * FROM conf", new PropertiesSQLResult());
// Upgrade DB needed?
DBConnection referenceDB = new DBConnection(DBConnection.DBMS.SQLite, DEFAULT_DB_FILE);
Properties defaultDBConf =
referenceDB.exec("SELECT * FROM conf", new PropertiesSQLResult());
// Check DB version
logger.fine("DB version: "+ dbConf.getProperty(PROPERTY_DB_VERSION));
int defaultDBVersion = Integer.parseInt(defaultDBConf.getProperty(PROPERTY_DB_VERSION));
int dbVersion = Integer.parseInt(dbConf.getProperty(PROPERTY_DB_VERSION));
if(dbConf.getProperty(PROPERTY_DB_VERSION) == null || defaultDBVersion > dbVersion ) {
logger.info("Starting DB upgrade...");
File backupDB = FileUtil.getNextFile(dbFile);
logger.fine("Backing up DB to: "+ backupDB);
FileUtil.copy(dbFile, backupDB);
logger.fine(String.format("Upgrading DB (from: v%s, to: v%s)...",
dbConf.getProperty(PROPERTY_DB_VERSION),
defaultDBConf.getProperty(PROPERTY_DB_VERSION)));
DBUpgradeHandler handler = new DBUpgradeHandler(referenceDB);
handler.setTargetDB(db);
//handler.setForcedDBUpgrade(true);
handler.upgrade();
logger.info("DB upgrade done");
dbConf.setProperty(PROPERTY_DB_VERSION, defaultDBConf.getProperty(PROPERTY_DB_VERSION));
storeProperties();
}
referenceDB.close();
} catch (Exception e){
throw new RuntimeException(e);
}
}
public static String getStringProperty(String key){
String value = fileConf.getProperty(key);
if(value == null)
value = dbConf.getProperty(key);
return value;
}
public static int getIntegerProperty(String key){
return Integer.parseInt(getStringProperty(key));
}
public synchronized static void storeProperties() throws SQLException {
logger.fine("Saving conf to DB...");
PreparedStatement stmt = db.getPreparedStatement("REPLACE INTO conf (key, value) VALUES (?, ?)");
for(Object key : dbConf.keySet()){
stmt.setObject(1, key);
stmt.setObject(2, dbConf.get(key));
stmt.addBatch();
}
DBConnection.execBatch(stmt);
}
public static DBConnection getDB(){
return db;
}
}

View file

@ -45,11 +45,11 @@ public class DataAggregatorDaemon extends TimerTask implements HalDaemon {
logger.fine("The sensor is of type: " + sensor.getType());
if(sensor.getType().equals("PowerMeter")){
logger.fine("aggregating raw data to five minute periods");
aggregateRawData(sensor.getId(), TimeUtility.FIVE_MINUTES_IN_MS, 5, sensor.getAggregationMethod());
aggregateRawData(sensor, TimeUtility.FIVE_MINUTES_IN_MS, 5);
logger.fine("aggregating five minute periods into hour periods");
aggrigateAggregatedData(sensor.getId(), TimeUtility.FIVE_MINUTES_IN_MS, TimeUtility.HOUR_IN_MS, 12, sensor.getAggregationMethod());
aggrigateAggregatedData(sensor, TimeUtility.FIVE_MINUTES_IN_MS, TimeUtility.HOUR_IN_MS);
logger.fine("aggregating one hour periods into one day periods");
aggrigateAggregatedData(sensor.getId(), TimeUtility.HOUR_IN_MS, TimeUtility.DAY_IN_MS, 24, sensor.getAggregationMethod());
aggrigateAggregatedData(sensor, TimeUtility.HOUR_IN_MS, TimeUtility.DAY_IN_MS);
}else{
logger.fine("The sensor type is not supported by the aggregation daemon. Ignoring");
}
@ -60,7 +60,9 @@ public class DataAggregatorDaemon extends TimerTask implements HalDaemon {
* @param sensorId The sensor for to aggregate data
* @param toPeriodSizeInMs The period length in ms to aggregate to
*/
private void aggregateRawData(long sensorId, long toPeriodSizeInMs, int expectedSampleCount, AggregationMethod aggrMethod){
private void aggregateRawData(HalSensor sensor, long toPeriodSizeInMs, int expectedSampleCount){
long sensorId = sensor.getId();
AggregationMethod aggrMethod = sensor.getAggregationMethod();
DBConnection db = HalContext.getDB();
PreparedStatement stmt = null;
try {
@ -73,8 +75,8 @@ public class DataAggregatorDaemon extends TimerTask implements HalDaemon {
Long maxTimestampFoundForSensor = DBConnection.exec(stmt, new SimpleSQLResult<Long>());
if(maxTimestampFoundForSensor == null)
maxTimestampFoundForSensor = 0l;
long currentPeriodStartTimestamp = TimeUtility.getTimestampPeriodStart(toPeriodSizeInMs, System.currentTimeMillis());
logger.fine("Calculating periods... (from:"+ maxTimestampFoundForSensor +", to:"+ currentPeriodStartTimestamp +")");
long currentPeriodStartTimestamp = TimeUtility.getTimestampPeriodStart_UTC(toPeriodSizeInMs, System.currentTimeMillis());
logger.fine("Calculating periods... (from:"+ maxTimestampFoundForSensor +", to:"+ currentPeriodStartTimestamp +") with expected sample count: " + expectedSampleCount);
stmt = db.getPreparedStatement("SELECT *, 1 AS confidence, timestamp AS timestamp_start FROM sensor_data_raw"
+" WHERE sensor_id == ?"
+ " AND ? < timestamp"
@ -95,7 +97,10 @@ public class DataAggregatorDaemon extends TimerTask implements HalDaemon {
* @param fromPeriodSizeInMs The period length in ms to aggregate from
* @param toPeriodSizeInMs The period length in ms to aggregate to
*/
private void aggrigateAggregatedData(long sensorId, long fromPeriodSizeInMs, long toPeriodSizeInMs, int expectedSampleCount, AggregationMethod aggrMethod){
private void aggrigateAggregatedData(HalSensor sensor, long fromPeriodSizeInMs, long toPeriodSizeInMs){
long sensorId = sensor.getId();
AggregationMethod aggrMethod = sensor.getAggregationMethod();
int expectedSampleCount = (int)Math.ceil((double)toPeriodSizeInMs / (double)fromPeriodSizeInMs);
DBConnection db = HalContext.getDB();
PreparedStatement stmt = null;
try {
@ -108,8 +113,8 @@ public class DataAggregatorDaemon extends TimerTask implements HalDaemon {
Long maxTimestampFoundForSensor = DBConnection.exec(stmt, new SimpleSQLResult<Long>());
if(maxTimestampFoundForSensor == null)
maxTimestampFoundForSensor = 0l;
long currentPeriodStartTimestamp = TimeUtility.getTimestampPeriodStart(toPeriodSizeInMs, System.currentTimeMillis());
logger.fine("Calculating periods... (from:"+ maxTimestampFoundForSensor +", to:"+ currentPeriodStartTimestamp +")");
long currentPeriodStartTimestamp = TimeUtility.getTimestampPeriodStart_UTC(toPeriodSizeInMs, System.currentTimeMillis());
logger.fine("Calculating periods... (from:"+ maxTimestampFoundForSensor +", to:"+ currentPeriodStartTimestamp +") with expected sample count: " + expectedSampleCount);
stmt = db.getPreparedStatement("SELECT * FROM sensor_data_aggr"
+" WHERE sensor_id == ?"
@ -160,7 +165,7 @@ public class DataAggregatorDaemon extends TimerTask implements HalDaemon {
throw new IllegalArgumentException("found entry for aggregation for the wrong sensorId (expecting: "+sensorId+", but was: "+result.getInt("sensor_id")+")");
}
long timestamp = result.getLong("timestamp_start");
long periodTimestamp = TimeUtility.getTimestampPeriodStart(this.aggrTimeInMs, timestamp);
long periodTimestamp = TimeUtility.getTimestampPeriodStart_UTC(this.aggrTimeInMs, timestamp);
if(currentPeriodTimestamp != 0 && periodTimestamp != currentPeriodTimestamp){
float aggrConfidence = confidenceSum / (float)this.expectedSampleCount;
float data = -1;

View file

@ -42,15 +42,14 @@ public class DataDeletionDaemon extends TimerTask implements HalDaemon {
public void cleanupSensor(HalSensor sensor) {
logger.fine("The sensor is of type: " + sensor.getType());
if(sensor.getType().equals("PowerMeter")){
//if(sensor.isInternal()){ //TODO
cleanupInternalSensorData(sensor.getId(), TimeUtility.HOUR_IN_MS, TimeUtility.FIVE_MINUTES_IN_MS, TimeUtility.DAY_IN_MS);
cleanupInternalSensorData(sensor.getId(), TimeUtility.DAY_IN_MS, TimeUtility.HOUR_IN_MS, TimeUtility.WEEK_IN_MS);
//}else{ //TODO
//cleanupExternalSensorData(sensor.getId(), TimeUtility.FIVE_MINUTES_IN_MS, TimeUtility.DAY_IN_MS);
//cleanupExternalSensorData(sensor.getId(), TimeUtility.DAY_IN_MS, TimeUtility.WEEK_IN_MS);
//}
clearPeriodsOfWrongLenght(sensor.getId(), TimeUtility.FIVE_MINUTES_IN_MS, TimeUtility.HOUR_IN_MS, TimeUtility.WEEK_IN_MS);
if(sensor.getType().equals("PowerMeter")){ //TODO: use instanceof instead
if(sensor.getUser().isExternal()){
cleanupExternalSensorData(sensor.getId(), TimeUtility.FIVE_MINUTES_IN_MS, TimeUtility.DAY_IN_MS);
cleanupExternalSensorData(sensor.getId(), TimeUtility.DAY_IN_MS, TimeUtility.WEEK_IN_MS);
}else{
cleanupInternalSensorData(sensor.getId(), TimeUtility.HOUR_IN_MS, TimeUtility.FIVE_MINUTES_IN_MS, TimeUtility.DAY_IN_MS);
cleanupInternalSensorData(sensor.getId(), TimeUtility.DAY_IN_MS, TimeUtility.HOUR_IN_MS, TimeUtility.WEEK_IN_MS);
}
}else{
logger.fine("The sensor type is not supported by the cleanup deamon. Ignoring");
}
@ -69,7 +68,6 @@ public class DataDeletionDaemon extends TimerTask implements HalDaemon {
try {
Long maxDBTimestamp = null;
// delete too old 5 minute periods that already have been aggregated into hours
stmt = db.getPreparedStatement("SELECT MAX(timestamp_end) FROM sensor_data_aggr"
+" WHERE sensor_id == ? AND timestamp_end-timestamp_start == ?");
stmt.setLong(1, sensorId);
@ -117,32 +115,6 @@ public class DataDeletionDaemon extends TimerTask implements HalDaemon {
}
}
/**
* Will delete all aggregated entries for a sensor id where the period length is not expected
* @param sensorId
* @param expectedPeriodLengths
*/
private void clearPeriodsOfWrongLenght(long sensorId, long... expectedPeriodLengths){
DBConnection db = HalContext.getDB();
PreparedStatement stmt = null;
try {
StringBuilder querry = new StringBuilder("SELECT * FROM sensor_data_aggr WHERE sensor_id == ?");
for(int i = 0; i < expectedPeriodLengths.length; ++i){
querry.append(" AND timestamp_end-timestamp_start != ?");
}
stmt = db.getPreparedStatement(querry.toString());
for(int i = 0; i < expectedPeriodLengths.length; ++i){
stmt.setLong(i+1, expectedPeriodLengths[i]);
}
long deletedRows = DBConnection.exec(stmt, new AggregateDataDeleter(sensorId));
if(deletedRows > 0){
logger.severe("removed aggregated data with an unknown period length. Is the database corrupt?");
}
} catch (SQLException e) {
logger.log(Level.SEVERE, null, e);
}
}
private class AggregateDataDeleter implements SQLResultHandler<Long>{
private long sensorId = -1;

View file

@ -1,140 +1,151 @@
package se.koc.hal.plugin.localsensor;
import java.sql.SQLException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Logger;
import zutil.db.DBConnection;
import zutil.log.LogUtil;
import com.pi4j.io.gpio.GpioController;
import com.pi4j.io.gpio.GpioFactory;
import com.pi4j.io.gpio.GpioPinDigitalInput;
import com.pi4j.io.gpio.PinPullResistance;
import com.pi4j.io.gpio.PinState;
import com.pi4j.io.gpio.RaspiPin;
import com.pi4j.io.gpio.event.GpioPinDigitalStateChangeEvent;
import com.pi4j.io.gpio.event.GpioPinListenerDigital;
public class ImpulseTracker implements Runnable {
private static final Logger logger = LogUtil.getLogger();
private static final int IMPULSE_REPORT_TIMEOUT = 60000; //one minute
private long nanoSecondsSleep = IMPULSE_REPORT_TIMEOUT * 1000000L;
private Integer impulseCount = 0;
private ExecutorService executorPool;
private final DBConnection db;
private final int sensorId;
public static void main(String args[]) throws Exception {
new ImpulseTracker(2);
}
/**
* Constructor
* @param sensorId The ID of this sensor. Will be written to the DB
* @throws Exception
*/
public ImpulseTracker(int sensorId) throws Exception{
this.sensorId = sensorId;
// create gpio controller
final GpioController gpio = GpioFactory.getInstance();
// provision gpio pin #02 as an input pin with its internal pull up resistor enabled
final GpioPinDigitalInput irLightSensor = gpio.provisionDigitalInputPin(RaspiPin.GPIO_02, PinPullResistance.PULL_UP);
// create and register gpio pin listener. May require the program to be run as sudo if the GPIO pin has not been exported
irLightSensor.addListener(new GpioPinListenerDigital() {
@Override
public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent event) {
if(event.getState() == PinState.LOW){ //low = light went on
//System.out.println("IR LED turned ON");
synchronized(impulseCount){
impulseCount++;
}
}
}
});
// setup a thread pool for executing database jobs
this.executorPool = Executors.newCachedThreadPool();
// Connect to the database
logger.info("Connecting to db...");
db = new DBConnection(DBConnection.DBMS.SQLite, "hal.db");
//start a daemon thread to save the impulse count every minute
Thread thread = new Thread(this);
thread.setDaemon(false);
thread.start();
}
/**
* This loop will try to save the current time and the number of impulses seen every [IMPULSE_REPORT_TIMEOUT] milliseconds.
* Every iteration the actual loop time will be evaluated and used to calculate the time for the next loop.
*/
@Override
public void run() {
long startTime = System.nanoTime();
synchronized(impulseCount){
impulseCount = 0; //reset the impulse count
}
while(true) {
sleepNano(nanoSecondsSleep); //sleep for some time. This variable will be modified every loop to compensate for the loop time spent.
int count = -1;
synchronized(impulseCount){
count = impulseCount;
impulseCount = 0;
}
save(System.currentTimeMillis(), count); //save the impulse count
long estimatedNanoTimeSpent = System.nanoTime() - startTime; //this is where the loop ends
startTime = System.nanoTime(); //this is where the loop starts from now on
if(estimatedNanoTimeSpent > 0){ //if no overflow
long nanoSecondsTooMany = estimatedNanoTimeSpent - (IMPULSE_REPORT_TIMEOUT*1000000L);
//System.out.println("the look took ~" + estimatedNanoTimeSpent + "ns. That is " + nanoSecondsTooMany/1000000L + "ms off");
nanoSecondsSleep -= nanoSecondsTooMany / 3; //divide by constant to take into account varaiations im loop time
}
}
}
/**
* Sleep for [ns] nanoseconds
* @param ns
*/
private void sleepNano(long ns){
//System.out.println("will go to sleep for " + ns + "ns");
try{
Thread.sleep(ns/1000000L, (int)(ns%1000000L));
}catch(InterruptedException e){
//ignore
}
}
/**
* Saves the data to the database.
* This method should block the caller as short time as possible.
* Try to make the time spent in the method the same for every call (low variation).
*
* @param timestamp_end
* @param data
*/
private void save(final long timestamp_end, final int data){
//offload the timed loop by not doing the db interaction in this thread.
executorPool.execute(new Runnable(){
@Override
public void run() {
try {
db.exec("INSERT INTO sensor_data_raw(timestamp, sensor_id, data) VALUES("+timestamp_end+", "+ImpulseTracker.this.sensorId+", "+data+")");
} catch (SQLException e) {
e.printStackTrace();
}
}
});
}
}
package se.koc.hal.plugin.localsensor;
import java.sql.SQLException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import zutil.db.DBConnection;
import zutil.log.LogUtil;
import com.pi4j.io.gpio.GpioController;
import com.pi4j.io.gpio.GpioFactory;
import com.pi4j.io.gpio.GpioPinDigitalInput;
import com.pi4j.io.gpio.Pin;
import com.pi4j.io.gpio.PinPullResistance;
import com.pi4j.io.gpio.PinState;
import com.pi4j.io.gpio.RaspiPin;
import com.pi4j.io.gpio.event.GpioPinDigitalStateChangeEvent;
import com.pi4j.io.gpio.event.GpioPinListenerDigital;
public class RPiImpulseCountSensor implements Runnable {
private static final Logger logger = LogUtil.getLogger();
private static final int REPORT_TIMEOUT = 60_000; //one minute
private long nanoSecondsSleep = REPORT_TIMEOUT * 1_000_000L;
private volatile Integer impulseCount = 0;
private ExecutorService executorPool;
private final DBConnection db;
private final int sensorId;
public static void main(String args[]) throws Exception {
new RPiImpulseCountSensor(2, RaspiPin.GPIO_02);
}
/**
* Constructor
* @param sensorId The ID of this sensor. Will be written to the DB
* @throws Exception
*/
public RPiImpulseCountSensor(int sensorId, Pin pin) throws Exception{
this.sensorId = sensorId;
// create gpio controller
GpioController gpio = null;
try{
gpio = GpioFactory.getInstance();
}catch(IllegalArgumentException e){
logger.log(Level.SEVERE, "", e);
throw e;
}catch(UnsatisfiedLinkError e){
logger.log(Level.SEVERE, "", e);
throw e;
}
// provision gpio pin as an input pin with its internal pull up resistor enabled
final GpioPinDigitalInput irLightSensor = gpio.provisionDigitalInputPin(pin, PinPullResistance.PULL_UP);
// create and register gpio pin listener. May require the program to be run as sudo if the GPIO pin has not been exported
irLightSensor.addListener(new GpioPinListenerDigital() {
@Override
public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent event) {
if(event.getState() == PinState.LOW){ //low = light went on
//System.out.println("IR LED turned ON");
synchronized(impulseCount){
impulseCount++;
}
}
}
});
// setup a thread pool for executing database jobs
this.executorPool = Executors.newCachedThreadPool();
// Connect to the database
logger.info("Connecting to db...");
db = new DBConnection(DBConnection.DBMS.SQLite, "hal.db");
//start a daemon thread to save the impulse count every minute
Thread thread = new Thread(this);
thread.setDaemon(false);
thread.start();
}
/**
* This loop will try to save the current time and the number of impulses seen every [IMPULSE_REPORT_TIMEOUT] milliseconds.
* Every iteration the actual loop time will be evaluated and used to calculate the time for the next loop.
*/
@Override
public void run() {
long startTime = System.nanoTime();
synchronized(impulseCount){
impulseCount = 0; //reset the impulse count
}
while(true) {
sleepNano(nanoSecondsSleep); //sleep for some time. This variable will be modified every loop to compensate for the loop time spent.
int count = -1;
synchronized(impulseCount){
count = impulseCount;
impulseCount = 0;
}
save(System.currentTimeMillis(), count); //save the impulse count
long estimatedNanoTimeSpent = System.nanoTime() - startTime; //this is where the loop ends
startTime = System.nanoTime(); //this is where the loop starts from now on
if(estimatedNanoTimeSpent > 0){ //if no overflow
long nanoSecondsTooMany = estimatedNanoTimeSpent - (REPORT_TIMEOUT*1000000L);
//System.out.println("the look took ~" + estimatedNanoTimeSpent + "ns. That is " + nanoSecondsTooMany/1000000L + "ms off");
nanoSecondsSleep -= nanoSecondsTooMany / 3; //divide by constant to take into account varaiations im loop time
}
}
}
/**
* Sleep for [ns] nanoseconds
* @param ns
*/
private void sleepNano(long ns){
//System.out.println("will go to sleep for " + ns + "ns");
try{
Thread.sleep(ns/1000000L, (int)(ns%1000000L));
}catch(InterruptedException e){
//ignore
}
}
/**
* Saves the data to the database.
* This method should block the caller as short time as possible.
* Try to make the time spent in the method the same for every call (low variation).
*
* @param timestamp_end
* @param data
*/
private void save(final long timestamp_end, final int data){
//offload the timed loop by not doing the db interaction in this thread.
executorPool.execute(new Runnable(){
@Override
public void run() {
try {
db.exec("INSERT INTO sensor_data_raw(timestamp, sensor_id, data) VALUES("+timestamp_end+", "+RPiImpulseCountSensor.this.sensorId+", "+data+")");
} catch (SQLException e) {
e.printStackTrace();
}
}
});
}
}

View file

@ -10,91 +10,109 @@ public class TimeUtility {
public static final long DAY_IN_MS = HOUR_IN_MS * 24;
public static final long WEEK_IN_MS = DAY_IN_MS * 7;
public static long getTimestampPeriodStart_UTC(long periodLengthInMs, long timestamp) throws NumberFormatException{
if(periodLengthInMs < 0 || timestamp < 0)
throw new NumberFormatException("argument must be positive");
return timestamp - (timestamp % periodLengthInMs);
}
/**
* Get the timstamp for the given timestamp floored with the period length. The result should point to the beginning of the timestamps period.
* @param periodLengthInMs The periods length to floor the timestamp with
* @param timestamp The timestamp to floor.
* @return
*/
public static long getTimestampPeriodStart(long periodLengthInMs, long timestamp){
if(periodLengthInMs < DAY_IN_MS){ //simple math if the period is less than a day long
return timestamp - (timestamp % periodLengthInMs);
}else{
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(timestamp);
boolean clear = false;
int days = millisecondsToDays(periodLengthInMs);
if(days > 0){
int currentDay = cal.get(Calendar.DAY_OF_YEAR);
cal.set(Calendar.DAY_OF_YEAR, (currentDay/days)*days);
clear = true;
}
int hours = millisecondsToHourOfDay(periodLengthInMs);
if(hours > 0){
int currentHour = cal.get(Calendar.HOUR_OF_DAY);
cal.set(Calendar.HOUR_OF_DAY, (currentHour/hours)*hours);
clear = true;
}else if(clear){
cal.set(Calendar.HOUR_OF_DAY, 0);
}
int minutes = millisecondsToMinuteOfHour(periodLengthInMs);
if(minutes > 0){
int currentMinute = cal.get(Calendar.MINUTE);
cal.set(Calendar.MINUTE, (currentMinute/minutes)*minutes);
clear = true;
}else if(clear){
cal.set(Calendar.MINUTE, 0);
}
int seconds = millisecondsToSecondOfMinute(periodLengthInMs);
if(seconds > 0){
int currentSecond = cal.get(Calendar.SECOND);
cal.set(Calendar.SECOND, (currentSecond/seconds)*seconds);
clear = true;
}else if(clear){
cal.set(Calendar.SECOND, 0);
}
int milliseconds = millisecondsToMillisecondInSecond(periodLengthInMs);
if(milliseconds > 0){
int currentMillisecond = cal.get(Calendar.MILLISECOND);
cal.set(Calendar.MILLISECOND, (currentMillisecond/milliseconds)*milliseconds);
}else if(clear){
cal.set(Calendar.MILLISECOND, 0);
}
return cal.getTimeInMillis();
public static long getTimestampPeriodStart_LOCAL(long periodLengthInMs, long timestamp) throws NumberFormatException{
if(periodLengthInMs < 0 || timestamp < 0)
throw new NumberFormatException("argument must be positive");
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(timestamp);
boolean clear = false;
int days = getDaysFromTimestamp(periodLengthInMs);
if(days > 0){
int currentDay = cal.get(Calendar.DAY_OF_YEAR);
cal.set(Calendar.DAY_OF_YEAR, (currentDay/days)*days);
clear = true;
}
int hours = getHourOfDayFromTimestamp(periodLengthInMs);
if(hours > 0){
int currentHour = cal.get(Calendar.HOUR_OF_DAY);
cal.set(Calendar.HOUR_OF_DAY, (currentHour/hours)*hours);
clear = true;
}else if(clear){
cal.set(Calendar.HOUR_OF_DAY, 0);
}
int minutes = getMinuteOfHourFromTimestamp(periodLengthInMs);
if(minutes > 0){
int currentMinute = cal.get(Calendar.MINUTE);
cal.set(Calendar.MINUTE, (currentMinute/minutes)*minutes);
clear = true;
}else if(clear){
cal.set(Calendar.MINUTE, 0);
}
int seconds = getSecondOfMinuteFromTimestamp(periodLengthInMs);
if(seconds > 0){
int currentSecond = cal.get(Calendar.SECOND);
cal.set(Calendar.SECOND, (currentSecond/seconds)*seconds);
clear = true;
}else if(clear){
cal.set(Calendar.SECOND, 0);
}
int milliseconds = getMillisecondInSecondFromTimestamp(periodLengthInMs);
if(milliseconds > 0){
int currentMillisecond = cal.get(Calendar.MILLISECOND);
cal.set(Calendar.MILLISECOND, (currentMillisecond/milliseconds)*milliseconds);
}else if(clear){
cal.set(Calendar.MILLISECOND, 0);
}
return cal.getTimeInMillis();
}
public static int millisecondsToMillisecondInSecond(long ms){
public static int getMillisecondInSecondFromTimestamp(long ms) throws NumberFormatException{
if(ms < 0)
throw new NumberFormatException("argument must be positive");
return (int) (ms % SECOND_IN_MS);
}
public static int millisecondsToSecondOfMinute(long ms){
public static int getSecondOfMinuteFromTimestamp(long ms) throws NumberFormatException{
if(ms < 0)
throw new NumberFormatException("argument must be positive");
return (int) ((ms % MINUTES_IN_MS) / SECOND_IN_MS);
}
public static int millisecondsToMinuteOfHour(long ms){
public static int getMinuteOfHourFromTimestamp(long ms) throws NumberFormatException{
if(ms < 0)
throw new NumberFormatException("argument must be positive");
return (int) ((ms % HOUR_IN_MS) / MINUTES_IN_MS);
}
public static int millisecondsToHourOfDay(long ms){
public static int getHourOfDayFromTimestamp(long ms) throws NumberFormatException{
if(ms < 0)
throw new NumberFormatException("argument must be positive");
return (int) ((ms % DAY_IN_MS) / HOUR_IN_MS);
}
public static int millisecondsToDays(long ms){
public static int getDaysFromTimestamp(long ms) throws NumberFormatException{
if(ms < 0)
throw new NumberFormatException("argument must be positive");
return (int) (ms / DAY_IN_MS);
}
public static String msToString(long ms){
public static String msToString(long ms) throws NumberFormatException{
if(ms < 0)
throw new NumberFormatException("argument must be positive");
String retval = "";
int days = millisecondsToDays(ms);
int days = getDaysFromTimestamp(ms);
retval += days + "days+";
int hours = millisecondsToHourOfDay(ms);
int hours = getHourOfDayFromTimestamp(ms);
retval += (hours<10?"0"+hours:hours);
int minutes = millisecondsToMinuteOfHour(ms);
int minutes = getMinuteOfHourFromTimestamp(ms);
retval += ":" + (minutes<10?"0"+minutes:minutes);
int seconds = millisecondsToSecondOfMinute(ms);
int seconds = getSecondOfMinuteFromTimestamp(ms);
retval += ":" + (seconds<10?"0"+seconds:seconds);
int milliseconds = millisecondsToMillisecondInSecond(ms);
int milliseconds = getMillisecondInSecondFromTimestamp(ms);
retval += "." + (milliseconds<100?"0"+(milliseconds<10?"0"+milliseconds:milliseconds):milliseconds);
return retval;
}

120
test/se/koc/hal/util/TimeUtilityTest.java Executable file → Normal file
View file

@ -9,19 +9,20 @@ import org.junit.Test;
import se.koc.hal.util.TimeUtility;
public class TimeUtilityTest {
private long currentTime;
private Calendar referenceCalendar;
private long currentTime_UTC;
private Calendar referenceCalendar_LOCAL;
@Before
public void setup(){
currentTime = System.currentTimeMillis();
referenceCalendar = Calendar.getInstance();
referenceCalendar.setTimeInMillis(currentTime);
currentTime_UTC = System.currentTimeMillis();
referenceCalendar_LOCAL = Calendar.getInstance();
referenceCalendar_LOCAL.setTimeInMillis(currentTime_UTC);
}
// Test flooring LOCAL time to the closes day
@Test
public void testDayStartForCurrentTime(){
long thisPeriodStartedAt = TimeUtility.getTimestampPeriodStart(TimeUtility.DAY_IN_MS, currentTime);
public void testDayStart_LOCAL_ForCurrentTime(){
long thisPeriodStartedAt = TimeUtility.getTimestampPeriodStart_LOCAL(TimeUtility.DAY_IN_MS, currentTime_UTC);
Calendar testCalendar = Calendar.getInstance();
testCalendar.setTimeInMillis(thisPeriodStartedAt);
@ -29,48 +30,106 @@ public class TimeUtilityTest {
assertEquals("second is wrong", 0, testCalendar.get(Calendar.SECOND));
assertEquals("minute is wrong", 0, testCalendar.get(Calendar.MINUTE));
assertEquals("hour is wrong", 0, testCalendar.get(Calendar.HOUR_OF_DAY));
assertEquals("day is wrong", referenceCalendar.get(Calendar.DAY_OF_YEAR), testCalendar.get(Calendar.DAY_OF_YEAR));
assertEquals("day is wrong", referenceCalendar_LOCAL.get(Calendar.DAY_OF_YEAR), testCalendar.get(Calendar.DAY_OF_YEAR));
}
// Test flooring LOCAL time to the closes hour
@Test
public void testHourStartForCurrentTime(){
long thisPeriodStartedAt = TimeUtility.getTimestampPeriodStart(TimeUtility.HOUR_IN_MS, currentTime);
public void testHourStart_LOCAL_ForCurrentTime(){
long thisPeriodStartedAt = TimeUtility.getTimestampPeriodStart_LOCAL(TimeUtility.HOUR_IN_MS, currentTime_UTC);
Calendar testCalendar = Calendar.getInstance();
testCalendar.setTimeInMillis(thisPeriodStartedAt);
assertEquals("millisecond is wrong", 0, testCalendar.get(Calendar.MILLISECOND));
assertEquals("second is wrong", 0, testCalendar.get(Calendar.SECOND));
assertEquals("minute is wrong", 0, testCalendar.get(Calendar.MINUTE));
assertEquals("hour is wrong", referenceCalendar.get(Calendar.HOUR_OF_DAY), testCalendar.get(Calendar.HOUR_OF_DAY));
assertEquals("day is wrong", referenceCalendar.get(Calendar.DAY_OF_YEAR), testCalendar.get(Calendar.DAY_OF_YEAR));
assertEquals("hour is wrong", referenceCalendar_LOCAL.get(Calendar.HOUR_OF_DAY), testCalendar.get(Calendar.HOUR_OF_DAY));
assertEquals("day is wrong", referenceCalendar_LOCAL.get(Calendar.DAY_OF_YEAR), testCalendar.get(Calendar.DAY_OF_YEAR));
}
// Test flooring LOCAL time to the closes minute
@Test
public void testMinuteStartForCurrentTime(){
long thisPeriodStartedAt = TimeUtility.getTimestampPeriodStart(TimeUtility.MINUTES_IN_MS, currentTime);
public void testMinuteStart_LOCAL_ForCurrentTime(){
long thisPeriodStartedAt = TimeUtility.getTimestampPeriodStart_LOCAL(TimeUtility.MINUTES_IN_MS, currentTime_UTC);
Calendar testCalendar = Calendar.getInstance();
testCalendar.setTimeInMillis(thisPeriodStartedAt);
assertEquals("millisecond is wrong", 0, testCalendar.get(Calendar.MILLISECOND));
assertEquals("second is wrong", 0, testCalendar.get(Calendar.SECOND));
assertEquals("minute is wrong", referenceCalendar.get(Calendar.MINUTE), testCalendar.get(Calendar.MINUTE));
assertEquals("hour is wrong", referenceCalendar.get(Calendar.HOUR_OF_DAY), testCalendar.get(Calendar.HOUR_OF_DAY));
assertEquals("day is wrong", referenceCalendar.get(Calendar.DAY_OF_YEAR), testCalendar.get(Calendar.DAY_OF_YEAR));
assertEquals("minute is wrong", referenceCalendar_LOCAL.get(Calendar.MINUTE), testCalendar.get(Calendar.MINUTE));
assertEquals("hour is wrong", referenceCalendar_LOCAL.get(Calendar.HOUR_OF_DAY), testCalendar.get(Calendar.HOUR_OF_DAY));
assertEquals("day is wrong", referenceCalendar_LOCAL.get(Calendar.DAY_OF_YEAR), testCalendar.get(Calendar.DAY_OF_YEAR));
}
// Test flooring LOCAL time to the closes second
@Test
public void testSecondStartForCurrentTime(){
long thisPeriodStartedAt = TimeUtility.getTimestampPeriodStart(TimeUtility.SECOND_IN_MS, currentTime);
public void testSecondStart_LOCAL_ForCurrentTime(){
long thisPeriodStartedAt = TimeUtility.getTimestampPeriodStart_LOCAL(TimeUtility.SECOND_IN_MS, currentTime_UTC);
Calendar testCalendar = Calendar.getInstance();
testCalendar.setTimeInMillis(thisPeriodStartedAt);
assertEquals("millisecond is wrong", 0, testCalendar.get(Calendar.MILLISECOND));
assertEquals("second is wrong", referenceCalendar.get(Calendar.SECOND), testCalendar.get(Calendar.SECOND));
assertEquals("minute is wrong", referenceCalendar.get(Calendar.MINUTE), testCalendar.get(Calendar.MINUTE));
assertEquals("hour is wrong", referenceCalendar.get(Calendar.HOUR_OF_DAY), testCalendar.get(Calendar.HOUR_OF_DAY));
assertEquals("day is wrong", referenceCalendar.get(Calendar.DAY_OF_YEAR), testCalendar.get(Calendar.DAY_OF_YEAR));
assertEquals("second is wrong", referenceCalendar_LOCAL.get(Calendar.SECOND), testCalendar.get(Calendar.SECOND));
assertEquals("minute is wrong", referenceCalendar_LOCAL.get(Calendar.MINUTE), testCalendar.get(Calendar.MINUTE));
assertEquals("hour is wrong", referenceCalendar_LOCAL.get(Calendar.HOUR_OF_DAY), testCalendar.get(Calendar.HOUR_OF_DAY));
assertEquals("day is wrong", referenceCalendar_LOCAL.get(Calendar.DAY_OF_YEAR), testCalendar.get(Calendar.DAY_OF_YEAR));
}
// Test flooring UTC time to the closes day
@Test
public void testDayStart_UTC_ForCurrentTime(){
long thisPeriodStartedAt = TimeUtility.getTimestampPeriodStart_UTC(TimeUtility.DAY_IN_MS, currentTime_UTC);
assertEquals("millisecond is wrong", 0, TimeUtility.getMillisecondInSecondFromTimestamp(thisPeriodStartedAt));
assertEquals("second is wrong", 0, TimeUtility.getSecondOfMinuteFromTimestamp(thisPeriodStartedAt));
assertEquals("minute is wrong", 0, TimeUtility.getMinuteOfHourFromTimestamp(thisPeriodStartedAt));
assertEquals("hour is wrong", 0, TimeUtility.getHourOfDayFromTimestamp(thisPeriodStartedAt));
assertEquals("day is wrong", TimeUtility.getDaysFromTimestamp(currentTime_UTC), TimeUtility.getDaysFromTimestamp(thisPeriodStartedAt));
}
// Test flooring UTC time to the closes hour
@Test
public void testHourStart_UTC_ForCurrentTime(){
long thisPeriodStartedAt = TimeUtility.getTimestampPeriodStart_UTC(TimeUtility.HOUR_IN_MS, currentTime_UTC);
Calendar testCalendar = Calendar.getInstance();
testCalendar.setTimeInMillis(thisPeriodStartedAt);
assertEquals("millisecond is wrong", 0, TimeUtility.getMillisecondInSecondFromTimestamp(thisPeriodStartedAt));
assertEquals("second is wrong", 0, TimeUtility.getSecondOfMinuteFromTimestamp(thisPeriodStartedAt));
assertEquals("minute is wrong", 0, TimeUtility.getMinuteOfHourFromTimestamp(thisPeriodStartedAt));
assertEquals("hour is wrong", TimeUtility.getHourOfDayFromTimestamp(currentTime_UTC), TimeUtility.getHourOfDayFromTimestamp(thisPeriodStartedAt));
assertEquals("day is wrong", TimeUtility.getDaysFromTimestamp(currentTime_UTC), TimeUtility.getDaysFromTimestamp(thisPeriodStartedAt));
}
// Test flooring UTC time to the closes minute
@Test
public void testMinuteStart_UTC_ForCurrentTime(){
long thisPeriodStartedAt = TimeUtility.getTimestampPeriodStart_UTC(TimeUtility.MINUTES_IN_MS, currentTime_UTC);
Calendar testCalendar = Calendar.getInstance();
testCalendar.setTimeInMillis(thisPeriodStartedAt);
assertEquals("millisecond is wrong", 0, TimeUtility.getMillisecondInSecondFromTimestamp(thisPeriodStartedAt));
assertEquals("second is wrong", 0, TimeUtility.getSecondOfMinuteFromTimestamp(thisPeriodStartedAt));
assertEquals("minute is wrong", TimeUtility.getMinuteOfHourFromTimestamp(currentTime_UTC), TimeUtility.getMinuteOfHourFromTimestamp(thisPeriodStartedAt));
assertEquals("hour is wrong", TimeUtility.getHourOfDayFromTimestamp(currentTime_UTC), TimeUtility.getHourOfDayFromTimestamp(thisPeriodStartedAt));
assertEquals("day is wrong", TimeUtility.getDaysFromTimestamp(currentTime_UTC), TimeUtility.getDaysFromTimestamp(thisPeriodStartedAt));
}
// Test flooring UTC time to the closes second
@Test
public void testSecondStart_UTC_ForCurrentTime(){
long thisPeriodStartedAt = TimeUtility.getTimestampPeriodStart_UTC(TimeUtility.SECOND_IN_MS, currentTime_UTC);
Calendar testCalendar = Calendar.getInstance();
testCalendar.setTimeInMillis(thisPeriodStartedAt);
assertEquals("millisecond is wrong", 0, TimeUtility.getMillisecondInSecondFromTimestamp(thisPeriodStartedAt));
assertEquals("second is wrong", TimeUtility.getSecondOfMinuteFromTimestamp(currentTime_UTC), TimeUtility.getSecondOfMinuteFromTimestamp(thisPeriodStartedAt));
assertEquals("minute is wrong", TimeUtility.getMinuteOfHourFromTimestamp(currentTime_UTC), TimeUtility.getMinuteOfHourFromTimestamp(thisPeriodStartedAt));
assertEquals("hour is wrong", TimeUtility.getHourOfDayFromTimestamp(currentTime_UTC), TimeUtility.getHourOfDayFromTimestamp(thisPeriodStartedAt));
assertEquals("day is wrong", TimeUtility.getDaysFromTimestamp(currentTime_UTC), TimeUtility.getDaysFromTimestamp(thisPeriodStartedAt));
}
// Test printing converting milliseconds to text
@Test
public void testMsToString(){
//low values
@ -82,7 +141,7 @@ public class TimeUtilityTest {
assertEquals("0days+01:00:00.000", TimeUtility.msToString(TimeUtility.HOUR_IN_MS));
assertEquals("1days+00:00:00.000", TimeUtility.msToString(TimeUtility.DAY_IN_MS));
assertEquals("7days+00:00:00.000", TimeUtility.msToString(TimeUtility.WEEK_IN_MS));
//high values
assertEquals("0days+00:00:00.999", TimeUtility.msToString(999));
assertEquals("0days+00:00:59.000", TimeUtility.msToString(TimeUtility.SECOND_IN_MS*59));
@ -90,9 +149,22 @@ public class TimeUtilityTest {
assertEquals("0days+23:00:00.000", TimeUtility.msToString(TimeUtility.HOUR_IN_MS*23));
assertEquals("369days+00:00:00.000", TimeUtility.msToString(TimeUtility.DAY_IN_MS*369));
//high overflow values
assertEquals("0days+00:00:01.999", TimeUtility.msToString(1999));
assertEquals("0days+00:02:39.000", TimeUtility.msToString(TimeUtility.SECOND_IN_MS*159));
assertEquals("0days+02:39:00.000", TimeUtility.msToString(TimeUtility.MINUTES_IN_MS*159));
assertEquals("5days+03:00:00.000", TimeUtility.msToString(TimeUtility.HOUR_IN_MS*123));
//combinations
long ms = (TimeUtility.DAY_IN_MS*999) + (TimeUtility.HOUR_IN_MS*23) + (TimeUtility.MINUTES_IN_MS*59) + (TimeUtility.SECOND_IN_MS*59) + 999;
assertEquals("999days+23:59:59.999", TimeUtility.msToString(ms));
}
// Test printing converting milliseconds to text for a negative time
@Test(expected=NumberFormatException.class)
public void testMsToStringForNegativeArgument(){
//low values
TimeUtility.msToString(-1);
}
}