#!/usr/bin/python -u
## The -u option above turns off block buffering of python output. This assures
## that each error message gets individually printed to the log file.
#
# Module: radmonAgent.py
#
# Description: This module acts as an agent between the radiation monitoring device
# and the Internet web server.  The agent periodically sends an http request to the
# radiation monitoring device and processes the response from the device and performs
# a number of operations:
#     - conversion of data items
#     - update a round robin (rrdtool) database with the radiation data
#     - periodically generate graphic charts for display in html documents
#     - forward the radiation data to other services
#     - write the processed weather data to a JSON file for use by html documents
#
# Copyright 2015 Jeff Owrey
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see http://www.gnu.org/license.
#
# Revision History
#   * v20 released 15 Sep 2015 by J L Owrey
#
import urllib2
import time
import calendar
import subprocess
import sys
import os
import json
import multiprocessing

    ### FILE AND FOLDER LOCATIONS ###

_USER = os.environ['USER']
_TMP_DIRECTORY = "/tmp/radmon" # folder for charts and output data file
_RRD_FILE = "/home/%s/database/radmonData.rrd" % _USER # database that stores the data
_OUTPUT_DATA_FILE = "/tmp/radmon/radmonData.js" # output file used by HTML docs

    ### GLOBAL CONSTANTS ###

_DEFAULT_WEB_DATA_UPDATE_INTERVAL = 10
_CHART_UPDATE_INTERVAL = 60 # defines how often the charts get updated
_DATABASE_UPDATE_INTERVAL = 30 # defines how often the database gets updated
_HTTP_REQUEST_TIMEOUT = 5 # number seconds to wait for a response to HTTP request
_CHART_WIDTH = 600
_CHART_HEIGHT = 150

   ### GLOBAL VARIABLES ###

webUpdateInterval = _DEFAULT_WEB_DATA_UPDATE_INTERVAL  # web update frequency
deviceUrl = "http://73.157.139.23:4371"  # radiation monitor network address
deviceOnline = True
debugOption = False


  ###  PRIVATE METHODS  ###

def getTimeStamp():
    """
    Sets the error message time stamp to the local system time.
    Parameters: none
    Returns string containing the time stamp.
    """
    return time.strftime( "%Y/%m/%d %T", time.localtime() )
##end def

def setOfflineStatus(dData):
    """Set the status of the the upstream device to "offline" and sends
       blank data to the downstream clients.
       Parameters:
           dData - dictionary object containing weather data
       Returns nothing.
    """
    global deviceOnline

    # If the radiation monitor was previously online, then send a message
    # that we are now offline.
    if deviceOnline:
        print "%s: radmon offline" % getTimeStamp()
        deviceOnline = False

    # Set data items to blank.
    dData['UTC'] = ''
    dData['CPM'] = ''
    dData['CPS'] = ''
    dData['uSvPerHr'] = ''
    dData['Mode'] = ''
    dData['status'] = 'offline'

    writeOutputDataFile(dData)
    return
##end def

  ###  PUBLIC METHODS  ###

def getRadmonData(deviceUrl, HttpRequestTimeout):
    """Send http request to radiation monitoring device.  The response
       from the device contains the radiation data.  The data is formatted
       as an html document.
    Parameters: 
        deviceUrl - url of radiation monitoring device
        HttpRequesttimeout - how long to wait for device
                             to respond to http request
    Returns a string containing the radiation data, or None if
    not successful.
    """
    global deviceOnline

    try:
        conn = urllib2.urlopen(deviceUrl + "/rdata", timeout=HttpRequestTimeout)
    except Exception, exError:
        # If no response is received from the device, then assume that
        # the device is down or unavailable over the network.  In
        # that case set the status of the device to offline.
        if debugOption:
            print "getRadmonData: %s\n" % exError
        return None

    # If the radiation monitor was previously offline, then send a message
    # that we are now online.
    if not deviceOnline:
        print "%s radmon online" % getTimeStamp()
        deviceOnline = True

    # Format received data into a single string.
    content = ""
    for line in conn:
         content += line.strip()
    del conn
    return content
##end def

def parseDataString(sData, dData):
    """Parse the radiation data JSON string from the radiation 
       monitoring device into its component parts.  
       Parameters:
           sData - the string containing the data to be parsed
           dData - a dictionary object to contain the parsed data items
       Returns true if successful, false otherwise.
    """
    try:
        sTmp = sData[2:-2]
        lsTmp = sTmp.split(',')
    except Exception, exError:
        print "%s parseDataString: %s" % (getTimeStamp(), exError)
        return False

    # Load the parsed data into a dictionary for easy access.
    for item in lsTmp:
        if "=" in item:
            dData[item.split('=')[0]] = item.split('=')[1]
    dData['status'] = 'online'

    return True
##end def

def convertData(dData):
    """Convert individual radiation data items as necessary.
       Parameters:
           lsData - a list object containing the radiation data
           dData - a dictionary object containing the radiation data
       Returns true if successful, false otherwise.
    """
    result = True
 
    try:
        # Convert UTC from radiation monitoring device to local time.
        ts_utc = time.strptime(dData['UTC'], "%H:%M:%S %m/%d/%Y")
        local_sec = calendar.timegm(ts_utc)
        dData['UTC'] = local_sec

        dData['Mode'] = dData['Mode'].lower()
        dData['uSvPerHr'] = dData.pop('uSv/hr')
    except Exception, exError:
        print "%s convertData: %s" % (getTimeStamp(), exError)
        result = False

    return result
##end def

def writeOutputDataFile(dData):
    """Convert individual weather string data items as necessary.
       Parameters:
           lsData - a list object containing the data to be written
                    to the JSON file
       Returns true if successful, false otherwise.
    """
    # Set date to current time and data
    dData['date'] = getTimeStamp()

    # Format the weather data as string using java script object notation.
    sData = '[{'
    for key in dData:
        sData += "\"%s\":\"%s\"," % (key, dData[key])
    sData = sData[:-1] + '}]'

    # Write the string to the output data file for use by html documents.
    try:
        fc = open(_OUTPUT_DATA_FILE, "w")
        fc.write(sData)
        fc.close()
    except Exception, exError:
        print "%s writeOutputDataFile: %s" % (getTimeStamp(), exError)
        return False

    if debugOption and 0:
        print sData

    return True
## end def

def updateDatabase(dData):
    """
    Updates the rrdtool database by executing an rrdtool system command.
    Formats the command using the data extracted from the radiation
    monitor response.   
    Parameters: dData - dictionary object containing data items to be
                        written to the rr database file
    Returns true if successful, false otherwise.
    """
    # The RR database stores whole units, so convert uSv to Sv.   
    Svvalue = float(dData['uSvPerHr']) * 1.0E-06 # convert micro-Sieverts to Sieverts

    # Create the rrdtool update command.
    strCmd = "rrdtool update %s %s:%s:%s" % \
                       (_RRD_FILE, dData['UTC'], dData['CPM'], Svvalue)
    if debugOption:
        print "%s\n" % strCmd # DEBUG

    # Run the command as a subprocess.
    try:
        subprocess.check_output(strCmd, shell=True,  \
                             stderr=subprocess.STDOUT)
    except subprocess.CalledProcessError, exError:
        print "%s: rrdtool update failed: %s" % \
                                 (getTimeStamp(), exError.output)
        return False

    return True
##end def

def createGraph(fileName, dataItem, gLabel, gTitle, gStart, lower, upper, addTrend):
    """Uses rrdtool to create a graph of specified weather data item.
       Parameters:
           fileName - name of graph image file
           dataItem - data item to be graphed
           gTitle - a title for the graph
           gStart - beginning time of the data to be graphed
       Returns true if successful, false otherwise.
    """
    gPath = _TMP_DIRECTORY + '/' + fileName + ".png"
    trendWindow = { 'end-1day': 7200,
                    'end-4weeks': 172800,
                    'end-12months': 604800 }
 
    # Format the rrdtool graph command.

    # Set chart start time, height, and width.
    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
   
    # Set the range of the chart ordinate dataum.
    if lower < upper:
        strCmd  +=  "-l %s -u %s " % (lower, upper)
    else:
        #strCmd += "-A -Y "
        strCmd += "-Y "

    # Set the chart ordinate label and chart title. 
    strCmd += "-v %s -t %s " % (gLabel, gTitle)

    # Show the data, or a moving average trend line over
    # the data, or both.
    strCmd += "DEF:%s=%s:%s:AVERAGE " % (dataItem, _RRD_FILE, dataItem)

    if addTrend == 0 or addTrend == 2:
        strCmd += "LINE1:%s\#0400ff " % (dataItem)
    if addTrend == 1 or addTrend == 2:
        strCmd += "CDEF:smoothed=%s,%s,TREND LINE1:smoothed#ff0000" \
                  % (dataItem, trendWindow[gStart])
       
    if debugOption:
        print "%s\n" % strCmd # DEBUG
    
    # Run the formatted rrdtool command as a subprocess.
    try:
        result = subprocess.check_output(strCmd, \
                     stderr=subprocess.STDOUT,   \
                     shell=True)
    except subprocess.CalledProcessError, exError:
        print "rrdtool graph failed: %s" % (exError.output)
        return False

    if debugOption:
        print "rrdtool graph: %s" % result
    return True

##end def

def getCLarguments():
    """Get command line arguments.  There are three possible arguments
          -d turns on debug mode
          -t sets the radiation device query interval
          -u sets the url of the radiation monitoring device
       Returns nothing.
    """
    global debugOption, webUpdateInterval, deviceUrl

    index = 1
    while index < len(sys.argv):
        if sys.argv[index] == '-d':
            debugOption = True
        elif sys.argv[index] == '-t':
            try:
                webUpdateInterval = abs(int(sys.argv[index + 1]))
            except:
                print "invalid polling period"
                exit(-1)
            index += 1
        elif sys.argv[index] == '-u':
            deviceUrl = sys.argv[index + 1]
            index += 1
        else:
            cmd_name = sys.argv[0].split('/')
            print "Usage: %s {-v} {-d}" % cmd_name[-1]
            exit(-1)
        index += 1
##end def

def generateGraphs():
    """Generate graphs for display in html documents.
       Parameters: none
       Returns nothing.
    """
    createGraph('radGraph1', 'CPM', "'counts per minute'", "'CPM - Last 24 Hours'", 'end-1day', 0, 0, 2)
    createGraph('radGraph2', 'SvperHr', "'Sv per hour'", "'Sv/Hr - Last 24 Hours'", 'end-1day', 0, 0, 2)
    createGraph('radGraph3', 'CPM', "'counts per minute'", "'CPM - Last 4 Weeks'", 'end-4weeks', 0, 0, 2)
    createGraph('radGraph4', 'SvperHr', "'Sv per hour'", "'Sv/Hr - Last 4 Weeks'", 'end-4weeks', 0, 0, 2)
    createGraph('radGraph5', 'CPM', "'counts per minute'", "'CPM - Past Year'", 'end-12months', 0, 0, 2)
    createGraph('radGraph6', 'SvperHr', "'Sv per hour'", "'Sv/Hr - Past Year'", 'end-12months', 0, 0, 2)
##end def

def main():
    """Handles timing of events and acts as executive routine managing all other
       functions.
       Parameters: none
       Returns nothing.
    """

    lastChartUpdateTime = - 1 # last time charts generated
    lastDatabaseUpdateTime = -1 # last time the rrdtool database updated
    lastWebUpdateTime = -1 # last time output JSON file updated
    dData = {}  # dictionary object for temporary data storage
    lsData = [] # list object for temporary data storage

    ## Get command line arguments.
    getCLarguments()

    ## Create www data folder if it does not already exist.
    if not os.path.isdir(_TMP_DIRECTORY):
        os.makedirs(_TMP_DIRECTORY)

    ## Exit with error if cannot find the rrdtool database file.
    if not os.path.exists(_RRD_FILE):
        print "cannot find rrdtool database file: terminating"
        exit(1)
 
    ## main loop
    while True:

        currentTime = time.time()

        # At the radiation device query interval request and process
        # the data from the device.
        if currentTime - lastWebUpdateTime > webUpdateInterval:
            lastWebUpdateTime = currentTime
            result = True

            # Get the data string from the device.
            sData = getRadmonData(deviceUrl, _HTTP_REQUEST_TIMEOUT)
            if sData == None:
                setOfflineStatus(dData)
                result = False

            # If successful parse the data.
            if result:
                result = parseDataString(sData, dData)

            # If parsing successful, convert the data.
            if result:
                result = convertData(dData)

            # If conversion successful, write data to output file.
            if result:
                writeOutputDataFile(dData)

        # At the rrdtool database update interval, update the database.
        if currentTime - lastDatabaseUpdateTime > _DATABASE_UPDATE_INTERVAL:   
            lastDatabaseUpdateTime = currentTime
            if result:
                ## Update the round robin database with the parsed data.
                result = updateDatabase(dData)

        # At the chart generation interval, generate charts.
        if currentTime - lastChartUpdateTime > _CHART_UPDATE_INTERVAL:
            lastChartUpdateTime = currentTime
            p = multiprocessing.Process(target=generateGraphs, args=())
            p.start()

        # Relinquish processing back to the operating system until
        # the next update interval.

        elapsedTime = time.time() - currentTime
        if debugOption:
            print "web update: %6f sec\n" % elapsedTime
        remainingTime = webUpdateInterval - elapsedTime
        if remainingTime > 0:
            time.sleep(remainingTime)
             
    ## end while
## end def

if __name__ == '__main__':
    main()