#!/usr/bin/python3 -u # # Module: rrdbase.py # # Description: This module acts as an interface between the agent module # the rrdtool command line app. Interface functions provide for updating # the rrdtool database and for creating charts. This module acts as a # library module that can be imported into and called from other # Python programs. # # Copyright 2021 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 Licensef # along with this program. If not, see http://www.gnu.org/license. # # Revision History # * v30 17 Oct 2021 by J L Owrey; first release # #2345678901234567890123456789012345678901234567890123456789012345678901234567890 import subprocess import time class rrdbase: def __init__(self, rrdFile, chartsDirectory, chartWidth, \ chartHeight, verboseMode, debugMode): """Initialize instance variables that remain constant throughout the life of this object instance. These items are set by the calling module. Parameters: rrdFile - the path to the rrdtool database file chartsDirectory - the path to the folder to contain charts chartWidth - the width of charts in pixels chartHeight - the height of charts in pixels verboseMode - verbose output debugMode - full debug output Returns: nothing """ self.rrdFile = rrdFile self.chartsDirectory = chartsDirectory self.chartWidth = chartWidth self.chartHeight = chartHeight self.verboseMode = verboseMode self.debugMode = debugMode ## end def def getTimeStamp(): """Sets the error message time stamp to the local system time. Parameters: none Returns: string containing the time stamp """ return time.strftime('%m/%d/%Y %H:%M:%S', time.localtime()) ## end def def getEpochSeconds(sTime): """Converts the time stamp supplied in the weather data string to seconds since 1/1/1970 00:00:00. Parameters: sTime - the time stamp to be converted must be formatted as %m/%d/%Y %H:%M:%S Returns: epoch seconds """ try: t_sTime = time.strptime(sTime, '%m/%d/%Y %H:%M:%S') except Exception as exError: print('%s getEpochSeconds: %s' % \ (rrdbase.getTimeStamp(), exError)) return None tSeconds = int(time.mktime(t_sTime)) return tSeconds ## end def def updateDatabase(self, *tData): """Updates the rrdtool round robin database with data supplied in the weather data string. Parameters: tData - a tuple object containing the data items to be written to the rrdtool database Returns: True if successful, False otherwise """ # Get the time stamp supplied with the data. This must always be # the first element of the tuple argument passed to this function. tData = list(tData) date = tData.pop(0) # Convert the time stamp to unix epoch seconds. try: time = rrdbase.getEpochSeconds(date) # Trap any data conversion errors. except Exception as exError: print('%s updateDatabase error: %s' % \ (rrdbase.getTimeStamp(), exError)) return False # Create the rrdtool command for updating the rrdtool database. Add a # '%s' format specifier for each data item remaining in tData. # Note that this is the list remaining after the # first item (the date) has been removed by the above code. strFmt = 'rrdtool update %s %s' + ':%s' * len(tData) strCmd = strFmt % ((self.rrdFile, time,) + tuple(tData)) if self.debugMode: print('%s' % strCmd) # DEBUG # Run the formatted command as a subprocess. try: subprocess.check_output(strCmd, stderr=subprocess.STDOUT, \ shell=True) except subprocess.CalledProcessError as exError: print('%s rrdtool update failed: %s' % \ (rrdbase.getTimeStamp(), exError.output.decode('utf-8'))) return False if self.verboseMode and not self.debugMode: print('database update successful') return True ## end def def createWeaGraph(self, fileName, dataItem, gLabel, gTitle, gStart, lower, upper, addTrend, autoScale): """Uses rrdtool to create a graph of specified weather data item. Graphs are for display in html documents. Parameters: fileName - name of graph file dataItem - the weather data item to be graphed gLabel - string containing a graph label for the data item gTitle - string containing a title for the graph gStart - time from now when graph starts lower - lower bound for graph ordinate upper - upper bound for graph ordinate addTrend - 0, show only graph data 1, show only a trend line 2, show a trend line and the graph data autoScale - if True, then use vertical axis auto scaling (lower and upper parameters are ignored), otherwise use lower and upper parameters to set vertical axis scale Returns: True if successful, False otherwise """ gPath = self.chartsDirectory + fileName + '.png' # 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, self.chartWidth, self.chartHeight) # Set the range and scaling of the chart y-axis. if lower < upper: strCmd += '-l %s -u %s -r ' % (lower, upper) elif autoScale: strCmd += '-A ' 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, or both. strCmd += 'DEF:dSeries=%s:%s:AVERAGE ' % (self.rrdFile, dataItem) if addTrend == 0: strCmd += 'LINE1:dSeries#0400ff ' elif addTrend == 1: strCmd += 'CDEF:smoothed=dSeries,86400,TREND LINE2:smoothed#006600 ' elif addTrend == 2: strCmd += 'LINE1:dSeries#0400ff ' strCmd += 'CDEF:smoothed=dSeries,86400,TREND LINE2:smoothed#006600 ' # if wind plot show color coded wind direction if dataItem == 'windspeedmph': strCmd += 'DEF:wDir=%s:winddir:AVERAGE ' % (_RRD_FILE) strCmd += 'VDEF:wMax=dSeries,MAXIMUM ' strCmd += 'CDEF:wMaxScaled=dSeries,0,*,wMax,+,-0.15,* ' strCmd += 'CDEF:ndir=wDir,337.5,GE,wDir,22.5,LE,+,wMaxScaled,0,IF ' strCmd += 'CDEF:nedir=wDir,22.5,GT,wDir,67.5,LT,*,wMaxScaled,0,IF ' strCmd += 'CDEF:edir=wDir,67.5,GE,wDir,112.5,LE,*,wMaxScaled,0,IF ' strCmd += 'CDEF:sedir=wDir,112.5,GT,wDir,157.5,LT,*,wMaxScaled,0,IF ' strCmd += 'CDEF:sdir=wDir,157.5,GE,wDir,202.5,LE,*,wMaxScaled,0,IF ' strCmd += 'CDEF:swdir=wDir,202.5,GT,wDir,247.5,LT,*,wMaxScaled,0,IF ' strCmd += 'CDEF:wdir=wDir,247.5,GE,wDir,292.5,LE,*,wMaxScaled,0,IF ' strCmd += 'CDEF:nwdir=wDir,292.5,GT,wDir,337.5,LT,*,wMaxScaled,0,IF ' strCmd += 'AREA:ndir#0000FF:N ' # Blue strCmd += 'AREA:nedir#1E90FF:NE ' # DodgerBlue strCmd += 'AREA:edir#00FFFF:E ' # Cyan strCmd += 'AREA:sedir#00FF00:SE ' # Lime strCmd += 'AREA:sdir#FFFF00:S ' # Yellow strCmd += 'AREA:swdir#FF8C00:SW ' # DarkOrange strCmd += 'AREA:wdir#FF0000:W ' # Red strCmd += 'AREA:nwdir#FF00FF:NW ' # Magenta ##end if if self.debugMode: print('%s' % strCmd) # DEBUG # Run the formatted rrdtool command as a subprocess. try: result = subprocess.check_output(strCmd, \ stderr=subprocess.STDOUT, \ shell=True) except subprocess.CalledProcessError as exError: print('rrdtool graph failed: %s' % (exError.output.decode('utf-8'))) return False if self.verboseMode: print('rrdtool graph: %s' % result.decode('utf-8')) #, end='') return True ## end def def createAutoGraph(self, fileName, dataItem, gLabel, gTitle, gStart, lower, upper, addTrend, autoScale): """Uses rrdtool to create a graph of specified radmon data item. Parameters: fileName - name of file containing the graph dataItem - data item to be graphed gLabel - string containing a graph label for the data item gTitle - string containing a title for the graph gStart - beginning time of the graphed data lower - lower bound for graph ordinate #NOT USED upper - upper bound for graph ordinate #NOT USED addTrend - 0, show only graph data 1, show only a trend line 2, show a trend line and the graph data autoScale - if True, then use vertical axis auto scaling (lower and upper parameters are ignored), otherwise use lower and upper parameters to set vertical axis scale Returns: True if successful, False otherwise """ gPath = self.chartsDirectory + 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, self.chartWidth, self.chartHeight) # Set the range and scaling of the chart y-axis. if lower < upper: strCmd += "-l %s -u %s -r " % (lower, upper) elif autoScale: strCmd += "-A " 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:dSeries=%s:%s:LAST " % (self.rrdFile, dataItem) if addTrend == 0: strCmd += "LINE1:dSeries#0400ff " elif addTrend == 1: strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE2:smoothed#006600 " \ % trendWindow[gStart] elif addTrend == 2: strCmd += "LINE1:dSeries#0400ff " strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE2:smoothed#006600 " \ % trendWindow[gStart] if self.debugMode: print("%s" % strCmd) # DEBUG # Run the formatted rrdtool command as a subprocess. try: result = subprocess.check_output(strCmd, \ stderr=subprocess.STDOUT, \ shell=True) except subprocess.CalledProcessError as exError: print("rrdtool graph failed: %s" % (exError.output.decode('utf-8'))) return False if self.verboseMode: print("rrdtool graph: %s" % result.decode('utf-8')) #, end='') return True ##end def ## end class