/*****
 *
 * WeatherScanner
 *
 * This software connects to wireless weather sensors via ELV IPWE1 adapter
 * it records the reading from the sensors to a MySQL database
 *
 *****/

// I/O support
import java.io.InputStream;
import java.io.OutputStream;

// JDBC support
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Timestamp;

// logging: log4j 2
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
// import org.apache.logging.log4j.LogManager;
import org.apache.log4j.BasicConfigurator;

// telnet protocol support
import org.apache.commons.net.telnet.EchoOptionHandler;
import org.apache.commons.net.telnet.SuppressGAOptionHandler;
import org.apache.commons.net.telnet.TelnetClient;

/***
 * This is a based on an example of
 *  a trivial use of the TelnetClient class,
 * 
 ***/

// This class requires the IOUtil support class!
public final class WeatherScanner
{
	
	// Define a static logger variable so that it references the
	// Logger instance named "WeatherScanner".
        static Logger logger = Logger.getLogger(WeatherScanner.class);
	// LOG4J2: static Logger logger = LogManager.getLogger(WeatherScanner.class);

	OutputStream ostream;
	InputStream istream;
	TelnetClient tclient;

	int newSamples = 0;
    long maxwaitchars = 10000;
	
	WeatherScanner(String hostname, int port) {
		tclient = new TelnetClient();
		SuppressGAOptionHandler gah = new SuppressGAOptionHandler(false,false,true,true);
		EchoOptionHandler eh = new EchoOptionHandler(false,false,true,true);
		tclient.setDefaultTimeout(30000);
		logger.debug("Will connect to "+hostname+":"+port);
		logger.debug(" Network Timeout is "+tclient.getDefaultTimeout()+" ms.");
		try {
		    tclient.addOptionHandler(gah);
			tclient.addOptionHandler(eh);
			tclient.connect(hostname, port);
			logger.debug("Connected.");
		}
		catch (Exception e)
		{
			e.printStackTrace();
			System.exit(1);
		}
		istream = tclient.getInputStream();
		ostream = tclient.getOutputStream();
	}

	public final static String charCodeDisplay (int ccode) {
		if ((ccode>=32) && (ccode<127)) {
			  return " " + new String(Character.toChars(ccode));	
			} else {
			  return  " .";
			}
	}
	
	//
	// check if 2 timestamps are at maximum <maxDistms> milliseconds apart
	//
	public final static boolean timeInRange(Timestamp t1, Timestamp t2,
			                                long maxDistms) {
		long t1ms = t1.getTime();
		long t2ms = t2.getTime();
		return (Math.abs(t2ms-t1ms)<=maxDistms);
		
	}
			                                 
	//
	// send one byte to the remote IPWE1 server
	// to cover a flaw in IPWE1, a delay is necessary after the byte
	// to guarantee that only one byte per packet is received by IPWE1
	//
	public final void xmit(int cod, OutputStream ostr) {
		try {
			this.ostream.write(cod);
			if (ostr != null) {
				ostr.write(cod);
			}
			this.ostream.flush();
			Thread.sleep(100);
			} catch (Exception e) {
				logger.error("Error in xmit: ",e);
				System.exit(1);
			}
		logger.debug(String.format("xmit: sent 0x%02X - ", cod)+charCodeDisplay(cod));
	}
	//
	// receive one byte from the remote server
	// log if <inf> is not null
	//
	public final int recv(String inf) {
		int b=0;
		try {
		b = this.istream.read();
		} catch (Exception e) {
			logger.error("error in recv: ",e);
			System.exit(1);
		}
		if (inf!=null) {
		  logger.debug(String.format(inf+": got 0x%02X - ",b)+charCodeDisplay(b));
		}
		return b;
	}
	//
	// receive one byte from the remote server,
	// no special log prefix info given
	//
	public final int recv() {
		return recv("recv");
	}
	//
	// send a string to the remote IPWE1 server
	// 
	public final void xmit(String xstr, OutputStream ostr) {
		int p;
		int l = xstr.length();
		for (p=0;p<l;p++) {
			this.xmit(xstr.codePointAt(p), ostr);
		}
	}
	

	public final void waitFor(String waitstr, OutputStream ostr, String inf) {
		boolean waiting =true;
		int pos = 0;
		int c = 0;
		long pc = 0;                  // received byte counter
		while (waiting) {
			if (pc>this.maxwaitchars) {
		      logger.error("waitFor["+((inf!=null)?inf:"")+"]: maximum number of "+this.maxwaitchars+" characters exceeded.");
		      System.exit(1);
			}
			c = waitstr.codePointAt(pos);
			if (inf!=null) {
			  logger.debug(" Waiting for: "+charCodeDisplay(c));
			}
			try {
				c = this.recv(inf);
				pc++;
				if (ostr!=null) {
					ostr.write(c);
				}
			} catch (Exception e) {
				logger.error("Error in waitfor: ",e);
				System.exit(1);
			}
			if (c==waitstr.codePointAt(pos)) {
				pos++;
				if (pos>=waitstr.length())
					return;
			}
			else {
				pos = 0;
			}
		}
	}
	
	//
	// wait for string waitstr, prevent logging
	//
	public final void waitFor(String waitstr, OutputStream ostr) {
		this.waitFor(waitstr, ostr, null);
	}
	
	public final void disconnect() {
		try {
			this.tclient.disconnect();
		} catch (Exception e) {
			logger.error("Error in disconnect: ",e);
			System.exit(1);
		}
	}
	
	//
	// read the values of one temperature/humidity sensor <sensNum> attached to the IPWE1
	// and store them in the database using the prepared statement <st>
	//
	// @param number of sensor as known by the IPWE1
	// @param prepared statement to insert value into database 
	//
	public final void readSensor(int sensNum, PreparedStatement st) throws Exception {
		int lsb = 0;
		int msb = 0;
		this.xmit("S", null);
		this.waitFor("S", null,"readSensorWaitS");  // consume echo "S"
		this.xmit(sensNum, null);
		this.xmit("\r\n", null);
		lsb = this.recv("rx_cons_sensnum");        // consume echo sensor number byte
		lsb = this.recv("rx_cons_dummy");          // consume excess byte (don't know what)
		lsb = this.recv("rx_cons_sensid");         // consume result sensor number byte
		int temp = 0;
		int rh = 0;
		msb = this.recv("rx_temp_msb");
		lsb = this.recv("rx_temp_lsb");
		rh = this.recv("rx_rh");
		java.util.Date today = new java.util.Date();
		Timestamp rdt = new java.sql.Timestamp(today.getTime());   // store current time as time of reading
		temp = 256*msb+lsb;
		logger.debug("Sensor "+sensNum+": Temp="+temp+", Humid="+rh);
		// this.xmit("\r\n",null);
		this.waitFor("IPWE1> ", null,"readSensorWaitPrompt");
		if (st!=null) {
			st.setTimestamp(1, rdt);
			st.setInt(2, sensNum);
			st.setString(3, "TEMPDC");
			st.setFloat(4, (float) (temp/10.0));
			st.execute();                           // store temperature
			logger.debug("wrote temperature");
			st.setString(3, "RHUMID");
			st.setDouble(4, rh);
			st.execute();                           // store humidity
			logger.debug("wrote humidity");
		}
	}
	
	//
	// check if entry in SENSREAD is already there
	//
	public final boolean readAlreadyThere(Connection conn, int sensNum, String unit, Timestamp rtime, double val) {
		try {
			Timestamp lt =  new Timestamp(rtime.getTime()-30000);
			Timestamp ut = new Timestamp(rtime.getTime()+30000);
			Double lv = val-1.0;
			Double uv = val+1.0;
			PreparedStatement st = conn.prepareStatement("select READTIME, SENSOR, UNIT, VALUE "+
	                                                 "from SENSREAD where "+
				                                     " READTIME between ? and ? "+
	                                                 " and SENSOR=? and UNIT=? and VALUE between ? and ?");
		  st.setTimestamp(1, lt);
		  st.setTimestamp(2, ut);
		  st.setInt(3, sensNum);
		  st.setString(4, unit);
		  st.setDouble(5,lv);
		  st.setDouble(6,uv);
		  // Slogger.debug("readAlreadyThere: check for "+unit+" on "+sensNum+
		  //	             " between "+lt+".."+ut+" value between "+lv+".."+uv);
		  ResultSet rs = st.executeQuery();
     	   // logger.debug("readAlreadyThere: query executed");
     	   String tim;
     	   String uni;
     	   int sen;
    	   while (rs.next()) {
    	            tim = rs.getString("READTIME");
    	            uni = rs.getString("UNIT");
    	            sen = rs.getInt("SENSOR");
    	            val = rs.getFloat("VALUE");
    				// logger.debug("readAlreadyThere: found: Time: "+tim+", Sensor: "+sen+", "+uni+": "+val);
    				return true;
    	   }
		} catch (Exception e) {
			logger.error("error in readAlreadyThere: ",e);
		}
		// logger.debug("readAlreadyThere: nothing found");
		return false;
	}
	//
	// read sensor <sensNum>'s history
	//
	public final void readSensorHistory(int sensNum, Connection conn ) {
		PreparedStatement st;
		try {
		  st = conn.prepareStatement(
    			"INSERT into SENSREAD "+
    			"(READTIME,SENSOR,UNIT,VALUE) "+
    			"VALUES(?,?,?,?)");
		
		int lsb = 0;
		int msb = 0;
		this.xmit("H", null);
		this.waitFor("H", null,"readSensorHistoryconsH");  // consume echo "H"
		this.xmit(sensNum, null);
		this.xmit("\r\n", null);
		lsb = this.recv("readSensorHistoryconsN");         // consume echo sensor number byte
		if (lsb!=sensNum) {
			logger.error("readSensorHistoryIllSensNumEcho: Echo of sensor number should be "+sensNum+" but was "+lsb);
		}
		lsb = this.recv("readSensorHistoryconsV1");        // consume excess byte (don't know what)
		lsb = this.recv("readSensorHistoryconsV2");        // consume result sensor number byte
		int sn=0;
		int temp = 0;
		int rh = 0;
		int eltim = 0;
		double tempdc = 0.0;
		double eltimM;
		int xr;
		String eltimMS;
		Timestamp now;
		Timestamp rt;
		java.util.Date today;
		for (sn=1;sn<=5;sn++) {
	      lsb = this.recv("readSensorHistoryconsX1");          // consume excess byte (don't know what)
		  lsb = this.recv("readSensorHistoryconsX2");          // consume excess byte (don't know what)
		  msb = this.recv("readSensorHistoryrxETm");          // get elapsed time (in seconds?) MSB
		  lsb = this.recv("readSensorHistoryrxETl");          // get elapsed time (in seconds?) LSB
		  eltim = msb*256+lsb;
		  eltimM = Math.floor(eltim/60);
		  eltimMS = eltimM+" min "+(eltim-(eltimM*60))+" sek.";
		  msb = this.recv("readSensorHistoryrxtm");          // get temperature MSB
		  lsb = this.recv("readSensorHistoryrxtl");          // get temperature LSB
		  temp = 256*msb+lsb;
		  if (temp>1000) {
			  logger.error("readSensorHistoryIllTemp, illegal value for raw temperature, is "+temp);
			  System.exit(1);
		  }
		  tempdc = temp/10.0;
		  rh = this.recv("readSensorHistoryrxrh");           // get relative humidity
		  if (rh>100) {
			  logger.error("readSensorHistoryIllRH, illegal value for relative humidity, is "+rh);
			  System.exit(1);
		  }
		  today = new java.util.Date();
		  now = new java.sql.Timestamp(today.getTime());
		  rt = new Timestamp(now.getTime()-eltim*1000);
		  logger.debug(now + ": Sensor "+sensNum+" history entry "+sn+": temp="+temp/10.0+",humid="+rh+
				  ", elapsed_time="+eltimMS+" sent at "+rt);


		  if (!this.readAlreadyThere(conn,sensNum,"TEMPDC",rt,tempdc)) {
			st.setTimestamp(1,rt);
			st.setInt(2,sensNum);
			st.setString(3,"TEMPDC");
			st.setDouble(4,tempdc);
			xr = st.executeUpdate();
			logger.debug("readSensorHistory: "+xr+" rows inserted of TEMPDC");
			this.newSamples += xr;
		  }
		  if  (!this.readAlreadyThere(conn,sensNum,"RHUMID",rt,rh)) {
				st.setTimestamp(1,rt);
				st.setInt(2,sensNum);
				st.setString(3,"RHUMID");
				st.setDouble(4,rh);
				xr = st.executeUpdate();
				logger.debug("readSensorHistory: "+xr+" rows inserted of RHUMID");
				this.newSamples += xr;
			  }
		}
		} catch (Exception e) {
			logger.error("error in readSensorHistory: ",e);
			System.exit(1);
		}
		this.waitFor("IPWE1> ", null,"readSensorHistorywaitPrompt");
	}
	
	//
	// main method for reading and recording T/H sensors 1 and 4 
	//
    public final static void main(String[] args)
    {
    	// Set up a simple configuration that logs on the console.
        BasicConfigurator.configure();
        logger.setLevel(Level.ALL);
    	
    	WeatherScanner w = new WeatherScanner("scherer3002.ddns.net",40023);
    	logger.info("WeatherScanner set up");
    	
    	// log into this IPWE1 ...
    	//
    	w.waitFor("Username: ", System.out);
    	w.xmit("admin\r\n", null);
    	w.waitFor("Password: ", System.out);
    	w.xmit("8May2K051\r\n", null);
    	w.waitFor("IPWE1>", System.out);
    	//
    	// execute a "status" command to make sensor status human readable
    	//
    	// w.xmit("status\r\n", System.out);
    	// w.waitFor("IPWE1>", System.out);
    	 
    	Connection conn = null;
    	try {
    	  conn = DriverManager.getConnection(
    			            "jdbc:mysql://scherer3002.ddns.net:43306/pucon","weatherscanner","8May2K051");
    	} catch (Exception e) {
    		logger.error("error connecting to database: "+e);
    		System.exit(1);
    	}
    	
    	/*
    	// ResultSet  rs = null;
    	PreparedStatement st;
    	
        try {
        	// conn = DriverManager.getConnection("jdbc:mysql://ursubuntu:3306/pucon","root","8May2K051");
           	conn = DriverManager.getConnection("jdbc:mysql://scherer3002.ddns.net:43306/pucon","weatherscanner","8May2K051");
        	logger.debug("got database connection");
        	    
        	st = conn.prepareStatement(
        			"INSERT into SENSREAD "+
        			"(READTIME,SENSOR,UNIT,VALUE) "+
        			"VALUES(?,?,?,?)");
    	//
    	// now read sensors 1 and 4
    	//
        logger.debug("Will read sensor 1");
    	w.readSensor(1,st);
        logger.debug("read Sensor 1, Will read sensor 4");
    	w.readSensor(4,st);
    	logger.debug("read sensor 4");
    	
        } catch (Exception e) {
        	logger.error("Error reading and saving sensors: ",e);
        	System.exit(1);
        }
        //
        // show sensor 1's history
        //
        w.xmit("sensor 1\r\n", System.out);
        w.waitFor("IPWE1> ", System.out);
        */
    	
    	// boolean running = true;
    	//
    	// read known sensor's history
    	//
    	// while (running) {
    	w.readSensorHistory(1,conn);
        w.readSensorHistory(4,conn);
        // logger.info(w.newSamples+" readings stored.");
        // try {
        // Thread.sleep(5000);
        // } catch (Exception e) {
        //	logger.error("Error in loop-sleep-60000: ",e);
        // }
    	// }
        
    	//
    	// ... and log off again
    	//
        logger.debug("will exit");
        w.xmit("exit\r\n", System.out);    

        logger.info("End: "+w.newSamples+" new values recorded");
        
        w.disconnect();
        
        logger.debug("Disconnected");
        
        //
        // for test purposes, read back all data stored in the sensors reading table
        //
        
        /* 
        int sens;
        float val;
        try {
       	   logger.debug("Starting readback");
       	   Statement stmt = conn.createStatement();
       	   rs = stmt.executeQuery("select * from SENSREAD");
       	   logger.debug("query executed");
       	   while (rs.next()) {
       	     String tim = rs.getString("READTIME");
       	     String unit = rs.getString("UNIT");
       	     sens = rs.getInt("SENSOR");
       	     val = rs.getFloat("VALUE");
       	     logger.debug("Time: "+tim+", Sensor: "+sens+", "+unit+": "+val);
       	   } 
        } catch (Exception e2) {
       	  logger.error("Exception: ",e2);
       	  System.exit(1);
       	}
        	 
        */
        System.exit(0);
        }
    }
