From 5036ec90e9aec4a73ba0a2cf5419e6ffda3cd807 Mon Sep 17 00:00:00 2001 From: Daniel Collin Date: Fri, 18 Dec 2015 10:00:27 +0100 Subject: [PATCH] Using UTC instead of LOCAL time. Former-commit-id: 21831d97d7f616313e0e90bf8ae69e04dea57685 --- src/se/koc/hal/HalContext.java | 233 +++++++------- .../koc/hal/deamon/DataAggregatorDaemon.java | 25 +- src/se/koc/hal/deamon/DataDeletionDaemon.java | 44 +-- ...racker.java => RPiImpulseCountSensor.java} | 291 +++++++++--------- src/se/koc/hal/util/TimeUtility.java | 130 ++++---- test/se/koc/hal/util/TimeUtilityTest.java | 120 ++++++-- 6 files changed, 461 insertions(+), 382 deletions(-) rename src/se/koc/hal/plugin/localsensor/{ImpulseTracker.java => RPiImpulseCountSensor.java} (82%) mode change 100755 => 100644 test/se/koc/hal/util/TimeUtilityTest.java diff --git a/src/se/koc/hal/HalContext.java b/src/se/koc/hal/HalContext.java index 22a38b3b..27e1fc13 100755 --- a/src/se/koc/hal/HalContext.java +++ b/src/se/koc/hal/HalContext.java @@ -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; + } + +} diff --git a/src/se/koc/hal/deamon/DataAggregatorDaemon.java b/src/se/koc/hal/deamon/DataAggregatorDaemon.java index 2cfab35c..7658d6f2 100755 --- a/src/se/koc/hal/deamon/DataAggregatorDaemon.java +++ b/src/se/koc/hal/deamon/DataAggregatorDaemon.java @@ -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()); 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()); 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; diff --git a/src/se/koc/hal/deamon/DataDeletionDaemon.java b/src/se/koc/hal/deamon/DataDeletionDaemon.java index 130d598c..5b305e7f 100755 --- a/src/se/koc/hal/deamon/DataDeletionDaemon.java +++ b/src/se/koc/hal/deamon/DataDeletionDaemon.java @@ -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{ private long sensorId = -1; diff --git a/src/se/koc/hal/plugin/localsensor/ImpulseTracker.java b/src/se/koc/hal/plugin/localsensor/RPiImpulseCountSensor.java similarity index 82% rename from src/se/koc/hal/plugin/localsensor/ImpulseTracker.java rename to src/se/koc/hal/plugin/localsensor/RPiImpulseCountSensor.java index fcc13659..44340867 100755 --- a/src/se/koc/hal/plugin/localsensor/ImpulseTracker.java +++ b/src/se/koc/hal/plugin/localsensor/RPiImpulseCountSensor.java @@ -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(); + } + } + }); + } + +} diff --git a/src/se/koc/hal/util/TimeUtility.java b/src/se/koc/hal/util/TimeUtility.java index 537ce348..66a1cb14 100755 --- a/src/se/koc/hal/util/TimeUtility.java +++ b/src/se/koc/hal/util/TimeUtility.java @@ -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; } diff --git a/test/se/koc/hal/util/TimeUtilityTest.java b/test/se/koc/hal/util/TimeUtilityTest.java old mode 100755 new mode 100644 index 0eef43ee..ac9e4790 --- a/test/se/koc/hal/util/TimeUtilityTest.java +++ b/test/se/koc/hal/util/TimeUtilityTest.java @@ -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); + } + }