#!/usr/bin/python

# Outback Mate datalogger
# Copyright 2009 Joe Martine

# Create a Class that gets updated continuously by values from the Mate.
# A separate routine will monitor the Class and record values as they
# change by a certain amount, or on a regular time schedule.

# Storing the data in three separate files.  The charge controller in one,
# for the DC monitor active data in one file and the day's totals in another.

# Rev 2 - Adding a curses display to show live data onscreen.
#         Also making a proper 'main' function


import serial
import time
import curses.wrapper

serport = '/dev/mate'

class Data():
	cc = []
	dclive = []
	dctotals = []
	ser = ''

	def __init__ (self, serport):
		for i in range(10):
			self.cc.append('0')

		for i in range(12):
			self.dclive.append('0')

		for i in range(36):
			self.dctotals.append('0')

		self.ser = serial.Serial (serport, 19200, timeout = 5)
		self.ser.setRTS(0)
		self.ser.setDTR(1)
		self.ser.flushInput()


	
	def update(self):
		flag = 0
		data = ''
		while flag == 0:
			byte = self.ser.read(1)

			if byte == '\n':
				data = self.ser.read(97)
				data = byte + data
				flag = 1

		# Having "bogus" data readings - logging everything raw.

#		file = open ('raw-log-' + time.strftime('%y%m%d'), 'a')
#		file.write (time.strftime('%H:%M:%S') + ',' + data + '\n')
#		file.close()

		# For FM-80, second byte will be 'B'.  DC will be 'b'.
		if data[:2] == '\nB':
			fmdata = data[:49]
			dcdata = data[49:]
		else:
			fmdata = data[49:]
			dcdata = data[:49]

		# FM-80
		# To save space, the savefile will only have strings of numbers.
		# 
		# Array holds ten entries:
		# 0 - Charge current
		# 1 - PV current
		# 2 - PV input voltage
		# 3 - Daily kWh
		# 4 - Aux output mode
		# 5 - Error mode
		# 6 - Charger mode
		# 7 - Battery voltage
		# 8 - Daily AH
		# 9 - Aux mode active

		self.cc[0] = fmdata[6:8] + '.' + fmdata[21]
		self.cc[1] = fmdata[9:11]
		self.cc[2] = fmdata[12:15]
		self.cc[3] = fmdata[16:18] + '.' + fmdata[18]
		self.cc[4] = fmdata[23:25]
		self.cc[5] = fmdata[26:29]
		self.cc[6] = fmdata[30:32]
		self.cc[7] = fmdata[33:35] + '.' + fmdata[35]
		self.cc[8] = fmdata[37:41]

		if int(self.cc[4]) > 63:
			self.cc[4] = str(int(self.cc[4]) - 64)
			self.cc[9] = '1'
		else:
			self.cc[9] = '0'


		# FM-DC
		# There are two arrays.  One for the "live" data, one for the "totals".
		#
		# 0, 1, 2 - Shunt A, B, C Current
		# 3 - Battery voltage
		# 4 - State-of-charge
		# 5, 6, 7 - Shunt A, B, C Enabled flags
		# 8 - Charge parameters met
		# 9 - Relay state
		# 10 - Relay mode
		# 11 - Battery temp

		self.dclive[0] = dcdata[3:6] + '.' + dcdata[6]
		self.dclive[1] = dcdata[8:11] + '.' + dcdata[11]
		self.dclive[2] = dcdata[13:16] + '.' + dcdata[16]
		self.dclive[3] = dcdata[27:29] + '.' + dcdata[29]
		self.dclive[4] = dcdata[31:34]
		self.dclive[5] = dcdata[35]
		self.dclive[6] = dcdata[36]
		self.dclive[7] = dcdata[37]
		self.dclive[11] = str( int(dcdata[42:44]) - 10)

		status = int (dcdata[39:41])
		if status > 31:
			status = status - 32
			self.dclive[2] = '-' + self.dclive[2]

		if status > 15:
			status = status - 16
			self.dclive[1] = '-' + self.dclive[1]

		if status > 7:
			status = status - 8
			self.dclive[0] = '-' + self.dclive[0]

		if status > 3:
			status = status - 4
			self.dclive[10] = '1'
		else:
			self.dclive[10] = '0'

		if status > 1:
			status = status - 2
			self.dclive[9] = '1'
		else:
			self.dclive[9] = '0'

		self.dclive[8] = str(status)


		# "Totals" array filled by the "extra data" bits.  This data is
		# not all sent continuously, the system incrementally steps through
		# each one every time it sends a string.  It's quite possible to 
		# miss one, so have to just rely on eventually catching them all...
		#
		# 0, 1 - Accumulated AH, kWh for shunt A
		# 2, 3 - Accumulated AH, kWh for shunt B
		# 4, 5 - Accumulated AH, kWh for shunt C
		# 6 - Days since full
		# 7 - Today's min SOC
		# 8, 9 - Today's net in/out AH
		# 10, 11 - Today's net in/out kWh
		# 12 - 35 - "reserved"  So far, only seen data for 12 & 13 but will keep space available.
		#
		# 12 appears to be "Battery Net In/Out"

		edId = int (dcdata[18:20])
		edValue = dcdata[21:26]

		if edId > 63:
			edValue = '-' + edValue
			edId = edId - 64

		if (edId == 1) or (edId == 3) or (edId == 5) or (edId == 10) or (edId == 11):
			self.dctotals[edId] = str( float(edValue) / 100)
		elif edId == 6:
			self.dctotals[edId] = str( float(edValue) / 10)
		else:
			self.dctotals[edId] = edValue

	
	def readCC(self):
		for i in range(10):
			print self.cc[i],

		print

	def readDClive(self):
		for i in range(12):
			print self.dclive[i],

		print

	def readDCtotals(self):
		for i in range(36):
			print self.dctotals[i],

		print



def logdata (logfile, data):
	datestamp = time.strftime('%y%m%d')
	datastring = time.strftime('%H:%M:%S')
	
	for i in range(len(data)):
		datastring = datastring + ',' + data[i]

	datastring = datastring + '\n'

	file = open(logfile+'-'+datestamp, 'a')
	file.write(datastring)
	file.close()



def checkData (data, lastData, delta, lastTime, updateIntvl):
	flag = 0
	if (time.time() - lastTime) >= updateIntvl:
		return True

	for i in range(len(data)):
		if delta[i] > 0:
			if abs( float(data[i]) - float(lastData[i])) >= delta[i]:
				return True
			
	return False


class Display():
	def __init__(self, stdscr):
		self.stdscr = stdscr

		# Get rid of the cursors
		curses.curs_set(0)

		# Set some preferred color pairs
		curses.init_pair (1, curses.COLOR_YELLOW, curses.COLOR_BLACK)
		curses.init_pair (2, curses.COLOR_BLUE, curses.COLOR_BLACK)
		curses.init_pair (3, curses.COLOR_GREEN, curses.COLOR_BLACK)
		curses.init_pair (4, curses.COLOR_RED, curses.COLOR_BLACK)

		# Set up windows on screen
		self.ccwin = curses.newwin(16, 40, 0, 0)
		self.fnlwin = curses.newwin(16, 40, 0, 40)
		self.fntwin = curses.newwin(8, 80, 16, 0)

		# Display background text
		self.redraw()

	
	def redraw(self):
		# Clear then redraw static text for each window.
		self.ccwin.clear()
		self.fnlwin.clear()
		self.fntwin.clear()
		
		self.ccwin.border()
		self.fnlwin.border()
		self.fntwin.border()

		self.ccwin.addstr(0,1, 'FM-80 Charge Controller', curses.color_pair(1))
		self.fnlwin.addstr(0,1, 'Flexnet DC Live', curses.color_pair(1))
		self.fntwin.addstr(0,1, 'Flexnet DC Totals', curses.color_pair(1))

		# Charge controller data tags:
		self.ccwin.addstr(2,2, 'Charger Mode:', curses.color_pair(2))
		self.ccwin.addstr(4,2, 'Charger:      V /      A', curses.color_pair(2))
		self.ccwin.addstr(6,2, 'PV Array:     V /    A', curses.color_pair(2))
		self.ccwin.addstr(8,2, 'Daily Total:      AH /      kWh', curses.color_pair(2))
		self.ccwin.addstr(10,2, 'Aux Output Mode:', curses.color_pair(2))
		self.ccwin.addstr(11,13, 'Active:', curses.color_pair(2))
		self.ccwin.addstr(13,2, 'Errors:', curses.color_pair(2))

		# FNDC Live
		self.fnlwin.addstr(2,2, 'Wind / Charger Current:        A', curses.color_pair(2))
		self.fnlwin.addstr(4,2, 'Solar Current:                 A', curses.color_pair(2))
		self.fnlwin.addstr(6,2, 'Load Current:                  A', curses.color_pair(2))
		self.fnlwin.addstr(8,2, 'Battery:      V /    degC', curses.color_pair(2))
		self.fnlwin.addstr(10,2, 'State of Charge:     %', curses.color_pair(2))
		self.fnlwin.addstr(12,2, 'Relay State:', curses.color_pair(2))
		self.fnlwin.addstr(13,2, 'Relay Mode:', curses.color_pair(2))

		# FNDC Totals
		self.fntwin.addstr(3,2, 'Shunt A:        AH /        kWh', curses.color_pair(2))
		self.fntwin.addstr(4,2, 'Shunt B:        AH /        kWh', curses.color_pair(2))
		self.fntwin.addstr(5,2, 'Shunt C:        AH /        kWh', curses.color_pair(2))
		self.fntwin.addstr(1,41, "Today's Min SOC:     %", curses.color_pair(2))
		self.fntwin.addstr(3,41, "Today's Net In:        AH /        kWh", curses.color_pair(2))
		self.fntwin.addstr(4,41, "Today's Net Out:       AH /        kWh", curses.color_pair(2))
		self.fntwin.addstr(5,41, "Today's Net Batt In/Out:        AH", curses.color_pair(2))
		self.fntwin.addstr(6,41, "Days Since Full:", curses.color_pair(2))

		self.ccwin.refresh()
		self.fnlwin.refresh()
		self.fntwin.refresh()



	def update(self, ccdata, fnldata, fntdata):
		# CC
		chgmodes = ['Snoozing', 'Float   ', 'Bulk    ', 'Absorb  ', 'Equalize']
		self.ccwin.addstr(2,16, chgmodes[int(ccdata[6])], curses.color_pair(3))

		self.ccwin.addstr(4,11, ccdata[7], curses.color_pair(3))
		self.ccwin.addstr(4,20, ccdata[0], curses.color_pair(3))
		self.ccwin.addstr(6,12, ccdata[2], curses.color_pair(3))
		self.ccwin.addstr(6,20, ccdata[1], curses.color_pair(3))
		self.ccwin.addstr(8,15, ccdata[8], curses.color_pair(3))
		self.ccwin.addstr(8,25, ccdata[3], curses.color_pair(3))

		auxmodes = ['Disabled     ', 'Diversion    ', 'Remote       ', 'Manual       ', 'Vent Fan     ', 'PV Trigger   ',
				'Float        ', 'ERROR Output ', 'Night Light  ', 'PWM Diversion', 'Low Battery  ']
		self.ccwin.addstr(10,19, auxmodes[int(ccdata[4])], curses.color_pair(3))
		auxstates = ['Off', 'On ']
		self.ccwin.addstr(11,21, auxstates[int(ccdata[9])], curses.color_pair(3))

		errors = ''
		errnum = int(ccdata[5])
		if errnum > 127:
			errnum = errnum - 128
			errors = "High VOC           "
		if errnum > 63:
			errnum = errnum - 64
			errors = 'Too Hot            '
		if errnum > 31:
			errors = 'Shorted Batt Sensor'

		self.ccwin.addstr(13,10, errors, curses.color_pair(3))


		# FNDC Live
		# Adding a little color for negative numbers...
		# Adding a leading space to non-negative numbers to keep columns in line.
		if float(fnldata[0])< 0:
			self.fnlwin.addstr(2,26, fnldata[0], curses.color_pair(4))
		else:
			self.fnlwin.addstr(2,26, ' '+fnldata[0], curses.color_pair(3))

		if float(fnldata[1])<0:
			self.fnlwin.addstr(4,26, fnldata[1], curses.color_pair(4))
		else:
			self.fnlwin.addstr(4,26, ' '+fnldata[1], curses.color_pair(3))

		if float(fnldata[2])<0:
			self.fnlwin.addstr(6,26, fnldata[2], curses.color_pair(4))
		else:
			self.fnlwin.addstr(6,26, ' '+fnldata[2], curses.color_pair(3))

		self.fnlwin.addstr(8,11, fnldata[3], curses.color_pair(3))
		self.fnlwin.addstr(8,20, fnldata[11], curses.color_pair(3))
		self.fnlwin.addstr(10,19, fnldata[4], curses.color_pair(3))

		cpm = ['          ', 'ChgPrmsMet']
		self.fnlwin.addstr(10,27, cpm[int(fnldata[8])], curses.color_pair(4))

		rlystate = ['Open  ', 'Closed']
		self.fnlwin.addstr(12,15, rlystate[int(fnldata[9])], curses.color_pair(3))
		rlymode = ['Auto  ', 'Manual']
		self.fnlwin.addstr(13,15, rlymode[int(fnldata[10])], curses.color_pair(3))


		# FNDC Totals
		if int(fntdata[0])<0:
			self.fntwin.addstr(3,11, fntdata[0], curses.color_pair(4))
		else:
			self.fntwin.addstr(3,11, ' '+fntdata[0], curses.color_pair(3))

		if float(fntdata[1])<0:
			self.fntwin.addstr(3,23, fntdata[1], curses.color_pair(4))
		else:
			self.fntwin.addstr(3,23, ' '+fntdata[1], curses.color_pair(3))

		if int(fntdata[2])<0:
			self.fntwin.addstr(4,11, fntdata[2], curses.color_pair(4))
		else:
			self.fntwin.addstr(4,11, ' '+fntdata[2], curses.color_pair(3))

		if float(fntdata[3])<0:
			self.fntwin.addstr(4,23, fntdata[3], curses.color_pair(4))
		else:
			self.fntwin.addstr(4,23, ' '+fntdata[3], curses.color_pair(3))

		if int(fntdata[4])<0:
			self.fntwin.addstr(5,11, fntdata[4], curses.color_pair(4))
		else:
			self.fntwin.addstr(5,11, ' '+fntdata[4], curses.color_pair(3))

		if float(fntdata[5])<0:
			self.fntwin.addstr(5,23, fntdata[5], curses.color_pair(4))
		else:
			self.fntwin.addstr(5,23, ' '+fntdata[5], curses.color_pair(3))

		self.fntwin.addstr(1,58, fntdata[7], curses.color_pair(3))
		self.fntwin.addstr(3,58, fntdata[8], curses.color_pair(3))
		self.fntwin.addstr(3,69, fntdata[10], curses.color_pair(3))
		self.fntwin.addstr(4,58, fntdata[9], curses.color_pair(3))
		self.fntwin.addstr(4,69, fntdata[11], curses.color_pair(3))

		if int(fntdata[12])<0:
			self.fntwin.addstr(5,66, fntdata[12], curses.color_pair(4))
		else:
			self.fntwin.addstr(5,66, ' '+fntdata[12], curses.color_pair(3))

		self.fntwin.addstr(6,58, fntdata[6], curses.color_pair(3))

		self.ccwin.refresh()
		self.fnlwin.refresh()
		self.fntwin.refresh()



def main(stdscr):
	screen = Display(stdscr)

	serdata = Data(serport)

	lastCC = []
	lastDCL = []
	lastDCT = []

	# Deltas - if zero, don't log on deltas.  Otherwise, if that value changes by
	#          set amount or more from the previous logged value, then log immediately.

	ccDelta = [0.5, 0, 5, 0, 1, 1, 1, 0.2, 0, 1]
	dclDelta = [0.5, 0.5, 0.5, 0.2, 1, 0, 0, 0, 1, 1, 1, 1]
	dctDelta = []
	for i in range(36):
		dctDelta.append(0)

	ccLastTime = 0
	dclLastTime = 0
	dctLastTime = 0

	# Intvls - always log after this many seconds since previous entry.

	ccIntvl = 600
	dclIntvl = 600
	dctIntvl = 600


	while 1:
		serdata.update()

		screen.update(serdata.cc, serdata.dclive, serdata.dctotals)

		if checkData(serdata.cc, lastCC, ccDelta, ccLastTime, ccIntvl):
			logdata ('cc', serdata.cc)
			ccLastTime = time.time()
			lastCC = list(serdata.cc)


		if checkData(serdata.dclive, lastDCL, dclDelta, dclLastTime, dclIntvl):
			logdata ('dcl', serdata.dclive)
			dclLastTime = time.time()
			lastDCL = list(serdata.dclive)


		if checkData(serdata.dctotals, lastDCT, dctDelta, dctLastTime, dctIntvl):
			logdata ('dct', serdata.dctotals)
			dctLastTime = time.time()
			lastDCT = list(serdata.dctotals)



if __name__=='__main__':
	curses.wrapper(main)


