Browse code

minor revisions

gandolf authored on 07/05/2022 19:24:50
Showing 2 changed files
... ...
@@ -44,12 +44,12 @@
44 44
 import os
45 45
 import sys
46 46
 import signal
47
-import subprocess
48 47
 import multiprocessing
49 48
 import time
50 49
 import calendar
51 50
 import json
52 51
 from urllib.request import urlopen
52
+import rrdbase
53 53
 
54 54
    ### ENVIRONMENT ###
55 55
 
... ...
@@ -60,7 +60,7 @@ _USE_RADMON_TIMESTAMP = True
60 60
    ### DEFAULT RADIATION MONITOR URL ###
61 61
 
62 62
 _DEFAULT_RADIATION_MONITOR_URL = \
63
-    "{your radiation monitor url}"
63
+    "http://192.168.1.24"
64 64
 
65 65
     ### FILE AND FOLDER LOCATIONS ###
66 66
 
... ...
@@ -75,10 +75,14 @@ _RRD_FILE = "/home/%s/database/radmonData.rrd" % _USER
75 75
 
76 76
     ### GLOBAL CONSTANTS ###
77 77
 
78
-# max number of failed data requests allowed
79
-_MAX_FAILED_DATA_REQUESTS = 3
78
+# maximum number of failed data requests allowed
79
+_MAX_FAILED_DATA_REQUESTS = 2
80
+# maximum number of http request retries  allowed
81
+_MAX_HTTP_RETRIES = 5
82
+# delay time between http request retries
83
+_HTTP_RETRY_DELAY = 1.119
80 84
 # interval in seconds between data requests
81
-_DEFAULT_DATA_REQUEST_INTERVAL = 2
85
+_DEFAULT_DATA_REQUEST_INTERVAL = 5
82 86
 # number seconds to wait for a response to HTTP request
83 87
 _HTTP_REQUEST_TIMEOUT = 3
84 88
 
... ...
@@ -96,12 +100,13 @@ _CHART_HEIGHT = 150
96 100
 # turn on or off of verbose debugging information
97 101
 verboseMode = False
98 102
 debugMode = False
103
+reportUpdateFails = False
99 104
 
100 105
 # The following two items are used for detecting system faults
101 106
 # and radiation monitor online or offline status.
102 107
 # count of failed attempts to get data from radiation monitor
103 108
 failedUpdateCount = 0
104
-# detected status of radiation monitor device
109
+httpRetries = 0
105 110
 radmonOnline = False
106 111
 
107 112
 # status of reset command to radiation monitor
... ...
@@ -111,6 +116,9 @@ radiationMonitorUrl = _DEFAULT_RADIATION_MONITOR_URL
111 116
 # web update frequency
112 117
 dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
113 118
 
119
+# rrdtool database interface handler
120
+rrdb = None
121
+
114 122
   ###  PRIVATE METHODS  ###
115 123
 
116 124
 def getTimeStamp():
... ...
@@ -120,7 +128,7 @@ def getTimeStamp():
120 128
     Returns: string containing the time stamp
121 129
     """
122 130
     return time.strftime( "%m/%d/%Y %T", time.localtime() )
123
-##end def
131
+## end def
124 132
 
125 133
 def setStatusToOffline():
126 134
     """Set the detected status of the radiation monitor to
... ...
@@ -139,7 +147,7 @@ def setStatusToOffline():
139 147
     if radmonOnline:
140 148
         print('%s radiation monitor offline' % getTimeStamp())
141 149
     radmonOnline = False
142
-##end def
150
+## end def
143 151
 
144 152
 def terminateAgentProcess(signal, frame):
145 153
     """Send a message to log when the agent process gets killed
... ...
@@ -149,11 +157,13 @@ def terminateAgentProcess(signal, frame):
149 157
            signal, frame - dummy parameters
150 158
        Returns: nothing
151 159
     """
160
+    # Inform downstream clients by removing output data file.
161
+    if os.path.exists(_OUTPUT_DATA_FILE):
162
+       os.remove(_OUTPUT_DATA_FILE)
152 163
     print('%s terminating radmon agent process' % \
153 164
               (getTimeStamp()))
154
-    setStatusToOffline()
155 165
     sys.exit(0)
156
-##end def
166
+## end def
157 167
 
158 168
   ###  PUBLIC METHODS  ###
159 169
 
... ...
@@ -165,8 +175,9 @@ def getRadiationData(dData):
165 175
        Returns: a string containing the radiation data if successful,
166 176
                 or None if not successful
167 177
     """
168
-    sUrl = radiationMonitorUrl
178
+    global httpRetries
169 179
 
180
+    sUrl = radiationMonitorUrl
170 181
     if remoteDeviceReset:
171 182
         sUrl += "/reset" # reboot the radiation monitor
172 183
     else:
... ...
@@ -187,19 +198,31 @@ def getRadiationData(dData):
187 198
         # If no response is received from the device, then assume that
188 199
         # the device is down or unavailable over the network.  In
189 200
         # that case return None to the calling function.
190
-        if verboseMode:
191
-            print("%s getRadiationData: %s" % (getTimeStamp(), exError))
192
-        return False
193
-    ##end try
201
+        httpRetries += 1
202
+
203
+        if reportUpdateFails:
204
+            print("%s " % getTimeStamp(), end='')
205
+        if reportUpdateFails or verboseMode:
206
+            print("http request failed (%d): %s" % \
207
+                (httpRetries, exError))
208
+
209
+        if httpRetries > _MAX_HTTP_RETRIES:
210
+            httpRetries = 0
211
+            return False
212
+        else:
213
+            time.sleep(_HTTP_RETRY_DELAY)
214
+            return getRadiationData(dData)
215
+    ## end try
194 216
 
195 217
     if debugMode:
196 218
         print(content)
197 219
     if verboseMode:
198 220
         print("http request successful: %.4f sec" % requestTime)
199 221
     
222
+    httpRetries = 0
200 223
     dData['content'] = content
201 224
     return True
202
-##end def
225
+## end def
203 226
 
204 227
 def parseDataString(dData):
205 228
     """Parse the data string returned by the radiation monitor
... ...
@@ -233,7 +256,7 @@ def parseDataString(dData):
233 256
     dData['serverMode'] = _SERVER_MODE
234 257
 
235 258
     return True
236
-##end def
259
+## end def
237 260
 
238 261
 def convertData(dData):
239 262
     """Convert individual radiation data items as necessary.
... ...
@@ -247,26 +270,27 @@ def convertData(dData):
247 270
             # device to epoch local time in seconds.
248 271
             ts_utc = time.strptime(dData['UTC'], "%H:%M:%S %m/%d/%Y")
249 272
             epoch_local_sec = calendar.timegm(ts_utc)
250
-            dData['ELT'] = epoch_local_sec
251 273
         else:
252 274
             # Use a timestamp generated by the requesting server (this)
253 275
             # instead of the timestamp provided by the radiation monitoring
254 276
             # device.  Using the server generated timestamp prevents errors
255 277
             # that occur when the radiation monitoring device fails to
256 278
             # synchronize with a valid NTP time server.
257
-            dData['ELT'] = time.time()
279
+            epoch_local_sec = time.time()
258 280
 
259 281
         dData['date'] = \
260
-            time.strftime("%m/%d/%Y %T", time.localtime(dData['ELT']))      
282
+            time.strftime("%m/%d/%Y %T", time.localtime(epoch_local_sec))      
261 283
         dData['mode'] = dData.pop('Mode').lower()
262
-        dData['uSvPerHr'] = '%.2f' % float(dData.pop('uSv/hr'))
284
+        dData['uSvPerHr'] = '%.2f' % float(dData['uSv/hr'])
285
+        # The rrdtool database stores whole units, so convert uSv to Sv.
286
+        dData['SvPerHr'] = float(dData.pop('uSv/hr')) * 1.0E-06
263 287
 
264 288
     except Exception as exError:
265 289
         print("%s data conversion failed: %s" % (getTimeStamp(), exError))
266 290
         return False
267 291
 
268 292
     return True
269
-##end def
293
+## end def
270 294
 
271 295
 def writeOutputFile(dData):
272 296
     """Write radiation data items to the output data file, formatted as 
... ...
@@ -321,124 +345,18 @@ def setRadmonStatus(updateSuccess):
321 345
             print('%s radiation monitor online' % getTimeStamp())
322 346
             radmonOnline = True
323 347
         return
324
-    elif failedUpdateCount == _MAX_FAILED_DATA_REQUESTS - 1:
348
+    else:
349
+        # The last attempt failed, so update the failed attempts
350
+        # count.
351
+        failedUpdateCount += 1
352
+
353
+    if failedUpdateCount == _MAX_FAILED_DATA_REQUESTS:
325 354
         # Max number of failed data requests, so set
326 355
         # device status to offline.
327 356
         setStatusToOffline()
328
-    ## end if
329
-    failedUpdateCount += 1
330
-##end def
331
-
332
-    ### DATABASE FUNCTIONS ###
333
-
334
-def updateDatabase(dData):
335
-    """
336
-    Update the rrdtool database by executing an rrdtool system command.
337
-    Format the command using the data extracted from the radiation
338
-    monitor response.   
339
-    Parameters: dData - dictionary object containing data items to be
340
-                        written to the rr database file
341
-    Returns: True if successful, False otherwise
342
-    """
343
-    global remoteDeviceReset
344
-
345
-    # The RR database stores whole units, so convert uSv to Sv.
346
-    SvPerHr = float(dData['uSvPerHr']) * 1.0E-06 
347
-
348
-    # Format the rrdtool update command.
349
-    strCmd = "rrdtool update %s %s:%s:%s" % \
350
-                       (_RRD_FILE, dData['ELT'], dData['CPM'], SvPerHr)
351
-    if debugMode:
352
-        print("%s" % strCmd) # DEBUG
353
-
354
-    # Run the command as a subprocess.
355
-    try:
356
-        subprocess.check_output(strCmd, shell=True,  \
357
-                             stderr=subprocess.STDOUT)
358
-    except subprocess.CalledProcessError as exError:
359
-        print("%s: rrdtool update failed: %s" % \
360
-                    (getTimeStamp(), exError.output))
361
-        if exError.output.find("illegal attempt to update using time") > -1:
362
-            remoteDeviceReset = True
363
-            print("%s: rebooting radiation monitor" % (getTimeStamp()))
364
-        return False
365
-
366
-    if verboseMode and not debugMode:
367
-        print("database update successful")
368
-
369
-    return True
370
-##end def
371
-
372
-def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
373
-                lower, upper, addTrend, autoScale):
374
-    """Uses rrdtool to create a graph of specified radmon data item.
375
-       Parameters:
376
-           fileName - name of file containing the graph
377
-           dataItem - data item to be graphed
378
-           gLabel - string containing a graph label for the data item
379
-           gTitle - string containing a title for the graph
380
-           gStart - beginning time of the graphed data
381
-           lower - lower bound for graph ordinate #NOT USED
382
-           upper - upper bound for graph ordinate #NOT USED
383
-           addTrend - 0, show only graph data
384
-                      1, show only a trend line
385
-                      2, show a trend line and the graph data
386
-           autoScale - if True, then use vertical axis auto scaling
387
-               (lower and upper parameters are ignored), otherwise use
388
-               lower and upper parameters to set vertical axis scale
389
-       Returns: True if successful, False otherwise
390
-    """
391
-    gPath = _CHARTS_DIRECTORY + fileName + ".png"
392
-    trendWindow = { 'end-1day': 7200,
393
-                    'end-4weeks': 172800,
394
-                    'end-12months': 604800 }
395
- 
396
-    # Format the rrdtool graph command.
397
-
398
-    # Set chart start time, height, and width.
399
-    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
400
-             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
401
-   
402
-    # Set the range and scaling of the chart y-axis.
403
-    if lower < upper:
404
-        strCmd  +=  "-l %s -u %s -r " % (lower, upper)
405
-    elif autoScale:
406
-        strCmd += "-A "
407
-    strCmd += "-Y "
408
-
409
-    # Set the chart ordinate label and chart title. 
410
-    strCmd += "-v %s -t %s " % (gLabel, gTitle)
411
- 
412
-    # Show the data, or a moving average trend line over
413
-    # the data, or both.
414
-    strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem)
415
-    if addTrend == 0:
416
-        strCmd += "LINE1:dSeries#0400ff "
417
-    elif addTrend == 1:
418
-        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE2:smoothed#006600 " \
419
-                  % trendWindow[gStart]
420
-    elif addTrend == 2:
421
-        strCmd += "LINE1:dSeries#0400ff "
422
-        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE2:smoothed#006600 " \
423
-                  % trendWindow[gStart]
424
-     
425
-    if debugMode:
426
-        print("\n%s" % strCmd) # DEBUG
427
-    
428
-    # Run the formatted rrdtool command as a subprocess.
429
-    try:
430
-        result = subprocess.check_output(strCmd, \
431
-                     stderr=subprocess.STDOUT,   \
432
-                     shell=True)
433
-    except subprocess.CalledProcessError as exError:
434
-        print("rrdtool graph failed: %s" % (exError.output))
435
-        return False
436
-
437
-    if verboseMode:
438
-        print("rrdtool graph: %s" % result.decode('utf-8'), end='')
439
-    return True
357
+## end def
440 358
 
441
-##end def
359
+    ### GRAPH FUNCTIONS ###
442 360
 
443 361
 def generateGraphs():
444 362
     """Generate graphs for display in html documents.
... ...
@@ -448,21 +366,21 @@ def generateGraphs():
448 366
     autoScale = False
449 367
 
450 368
     # past 24 hours
451
-    createGraph('24hr_cpm', 'CPM', 'counts\ per\ minute', 
369
+    rrdb.createAutoGraph('24hr_cpm', 'CPM', 'counts\ per\ minute', 
452 370
                 'CPM\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
453
-    createGraph('24hr_svperhr', 'SvperHr', 'Sv\ per\ hour',
371
+    rrdb.createAutoGraph('24hr_svperhr', 'SvperHr', 'Sv\ per\ hour',
454 372
                 'Sv/Hr\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
455 373
     # past 4 weeks
456
-    createGraph('4wk_cpm', 'CPM', 'counts\ per\ minute',
374
+    rrdb.createAutoGraph('4wk_cpm', 'CPM', 'counts\ per\ minute',
457 375
                 'CPM\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
458
-    createGraph('4wk_svperhr', 'SvperHr', 'Sv\ per\ hour',
376
+    rrdb.createAutoGraph('4wk_svperhr', 'SvperHr', 'Sv\ per\ hour',
459 377
                 'Sv/Hr\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
460 378
     # past year
461
-    createGraph('12m_cpm', 'CPM', 'counts\ per\ minute',
379
+    rrdb.createAutoGraph('12m_cpm', 'CPM', 'counts\ per\ minute',
462 380
                 'CPM\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
463
-    createGraph('12m_svperhr', 'SvperHr', 'Sv\ per\ hour',
381
+    rrdb.createAutoGraph('12m_svperhr', 'SvperHr', 'Sv\ per\ hour',
464 382
                 'Sv/Hr\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
465
-##end def
383
+## end def
466 384
 
467 385
 def getCLarguments():
468 386
     """Get command line arguments.  There are four possible arguments
... ...
@@ -473,7 +391,7 @@ def getCLarguments():
473 391
        Returns: nothing
474 392
     """
475 393
     global verboseMode, debugMode, dataRequestInterval, \
476
-           radiationMonitorUrl
394
+           radiationMonitorUrl, reportUpdateFails
477 395
 
478 396
     index = 1
479 397
     while index < len(sys.argv):
... ...
@@ -482,8 +400,16 @@ def getCLarguments():
482 400
         elif sys.argv[index] == '-d':
483 401
             verboseMode = True
484 402
             debugMode = True
485
-        elif sys.argv[index] == '-t':
486
-            dataRequestInterval = abs(int(sys.argv[index + 1]))
403
+        elif sys.argv[index] == '-r':
404
+            reportUpdateFails = True
405
+
406
+        # Update period and url options
407
+        elif sys.argv[index] == '-p':
408
+            try:
409
+                dataRequestInterval = abs(float(sys.argv[index + 1]))
410
+            except:
411
+                print("invalid polling period")
412
+                exit(-1)
487 413
             index += 1
488 414
         elif sys.argv[index] == '-u':
489 415
             radiationMonitorUrl = sys.argv[index + 1]
... ...
@@ -492,42 +418,49 @@ def getCLarguments():
492 418
             index += 1
493 419
         else:
494 420
             cmd_name = sys.argv[0].split('/')
495
-            print("Usage: %s [-d] [-t seconds] [-u url}" % cmd_name[-1])
421
+            print("Usage: %s [-d] [-p seconds] [-u url}" % cmd_name[-1])
496 422
             exit(-1)
497 423
         index += 1
498
-##end def
424
+## end def
499 425
 
500
-def main():
426
+def setup():
501 427
     """Handles timing of events and acts as executive routine managing
502 428
        all other functions.
503 429
        Parameters: none
504 430
        Returns: nothing
505 431
     """
506
-    signal.signal(signal.SIGTERM, terminateAgentProcess)
507
-    signal.signal(signal.SIGINT, terminateAgentProcess)
508
-
509
-    print('===================')
510
-    print('%s starting up radmon agent process' % \
511
-                  (getTimeStamp()))
512
-
513
-    # last time output JSON file updated
514
-    lastDataRequestTime = -1
515
-    # last time charts generated
516
-    lastChartUpdateTime = - 1
517
-    # last time the rrdtool database updated
518
-    lastDatabaseUpdateTime = -1
432
+    global rrdb
519 433
 
520 434
     ## Get command line arguments.
521 435
     getCLarguments()
522 436
 
437
+    print('====================================================')
438
+    print('%s starting up radmon agent process' % \
439
+                  (getTimeStamp()))
440
+
523 441
     ## Exit with error if rrdtool database does not exist.
524 442
     if not os.path.exists(_RRD_FILE):
525 443
         print('rrdtool database does not exist\n' \
526 444
               'use createRadmonRrd script to ' \
527 445
               'create rrdtool database\n')
528 446
         exit(1)
529
- 
530
-    ## main loop
447
+
448
+    signal.signal(signal.SIGTERM, terminateAgentProcess)
449
+    signal.signal(signal.SIGINT, terminateAgentProcess)
450
+
451
+    # Define object for calling rrdtool database functions.
452
+    rrdb = rrdbase.rrdbase( _RRD_FILE, _CHARTS_DIRECTORY, _CHART_WIDTH, \
453
+                            _CHART_HEIGHT, verboseMode, debugMode )
454
+## end def
455
+
456
+def loop():
457
+     # last time output JSON file updated
458
+    lastDataRequestTime = -1
459
+    # last time charts generated
460
+    lastChartUpdateTime = - 1
461
+    # last time the rrdtool database updated
462
+    lastDatabaseUpdateTime = -1
463
+
531 464
     while True:
532 465
 
533 466
         currentTime = time.time() # get current time in seconds
... ...
@@ -558,7 +491,8 @@ def main():
558 491
                            _DATABASE_UPDATE_INTERVAL):   
559 492
                 lastDatabaseUpdateTime = currentTime
560 493
                 ## Update the round robin database with the parsed data.
561
-                result = updateDatabase(dData)
494
+                result = rrdb.updateDatabase(dData['date'], \
495
+                             dData['CPM'], dData['SvPerHr'])
562 496
 
563 497
             # Set the radmon status to online or offline depending on the
564 498
             # success or failure of the above operations.
... ...
@@ -586,9 +520,9 @@ def main():
586 520
         if remainingTime > 0.0:
587 521
             time.sleep(remainingTime)
588 522
     ## end while
589
-    return
590 523
 ## end def
591 524
 
592 525
 if __name__ == '__main__':
593
-    main()
526
+    setup()
527
+    loop()
594 528
 
595 529
new file mode 100644
... ...
@@ -0,0 +1,290 @@
1
+#!/usr/bin/python3 -u
2
+#
3
+# Module: rrdbase.py
4
+#
5
+# Description: This module acts as an interface between the agent module
6
+# the rrdtool command line app.  Interface functions provide for updating
7
+# the rrdtool database and for creating charts. This module acts as a
8
+# library module that can be imported into and called from other
9
+# Python programs.
10
+#     
11
+# Copyright 2021 Jeff Owrey
12
+#    This program is free software: you can redistribute it and/or modify
13
+#    it under the terms of the GNU General Public License as published by
14
+#    the Free Software Foundation, either version 3 of the License, or
15
+#    (at your option) any later version.
16
+#
17
+#    This program is distributed in the hope that it will be useful,
18
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
+#    GNU General Public License for more details.
21
+#
22
+#    You should have received a copy of the GNU General Public Licensef
23
+#    along with this program.  If not, see http://www.gnu.org/license.
24
+#
25
+# Revision History
26
+#   * v30 17 Oct 2021 by J L Owrey; first release
27
+#
28
+#2345678901234567890123456789012345678901234567890123456789012345678901234567890
29
+
30
+import subprocess
31
+import time
32
+
33
+class rrdbase:
34
+
35
+    def __init__(self, rrdFile, chartsDirectory, chartWidth, \
36
+                 chartHeight, verboseMode, debugMode):
37
+        """Initialize instance variables that remain constant throughout
38
+           the life of this object instance.  These items are set by the
39
+           calling module.
40
+           Parameters:
41
+             rrdFile - the path to the rrdtool database file
42
+             chartsDirectory - the path to the folder to contain charts
43
+             chartWidth - the width of charts in pixels
44
+             chartHeight - the height of charts  in pixels
45
+             verboseMode - verbose output
46
+             debugMode - full debug output
47
+           Returns: nothing
48
+        """
49
+        self.rrdFile = rrdFile
50
+        self.chartsDirectory = chartsDirectory
51
+        self.chartWidth = chartWidth
52
+        self.chartHeight = chartHeight
53
+        self.verboseMode = verboseMode
54
+        self.debugMode = debugMode
55
+    ## end def
56
+
57
+    def getTimeStamp():
58
+        """Sets the error message time stamp to the local system time.
59
+           Parameters: none
60
+           Returns: string containing the time stamp
61
+        """
62
+        return time.strftime('%m/%d/%Y %H:%M:%S', time.localtime())
63
+    ## end def
64
+
65
+    def getEpochSeconds(sTime):
66
+        """Converts the time stamp supplied in the weather data string
67
+           to seconds since 1/1/1970 00:00:00.
68
+           Parameters: 
69
+               sTime - the time stamp to be converted must be formatted
70
+                       as %m/%d/%Y %H:%M:%S
71
+           Returns: epoch seconds
72
+        """
73
+        try:
74
+            t_sTime = time.strptime(sTime, '%m/%d/%Y %H:%M:%S')
75
+        except Exception as exError:
76
+            print('%s getEpochSeconds: %s' % \
77
+                  (rrdbase.getTimeStamp(), exError))
78
+            return None
79
+        tSeconds = int(time.mktime(t_sTime))
80
+        return tSeconds
81
+    ## end def
82
+
83
+    def updateDatabase(self, *tData):
84
+        """Updates the rrdtool round robin database with data supplied in
85
+           the weather data string.
86
+           Parameters:
87
+               tData - a tuple object containing the data items to be written
88
+                       to the rrdtool database
89
+           Returns: True if successful, False otherwise
90
+        """
91
+        # Get the time stamp supplied with the data.  This must always be
92
+        # the first element of the tuple argument passed to this function.
93
+        tData = list(tData)
94
+        date = tData.pop(0)
95
+        # Convert the time stamp to unix epoch seconds.
96
+        try:
97
+            time = rrdbase.getEpochSeconds(date)
98
+        # Trap any data conversion errors.
99
+        except Exception as exError:
100
+            print('%s updateDatabase error: %s' % \
101
+                  (rrdbase.getTimeStamp(), exError))
102
+            return False
103
+
104
+        # Create the rrdtool command for updating the rrdtool database.  Add a
105
+        # '%s' format specifier for each data item remaining in tData. 
106
+        # Note that this is the list remaining after the
107
+        # first item (the date) has been removed by the above code.
108
+        strFmt = 'rrdtool update %s %s' + ':%s' * len(tData)
109
+        strCmd = strFmt % ((self.rrdFile, time,) + tuple(tData))
110
+
111
+        if self.debugMode:
112
+            print('%s' % strCmd) # DEBUG
113
+
114
+        # Run the formatted command as a subprocess.
115
+        try:
116
+            subprocess.check_output(strCmd, stderr=subprocess.STDOUT, \
117
+                                    shell=True)
118
+        except subprocess.CalledProcessError as exError:
119
+            print('%s rrdtool update failed: %s' % \
120
+                  (rrdbase.getTimeStamp(), exError.output.decode('utf-8')))
121
+            return False
122
+
123
+        if self.verboseMode and not self.debugMode:
124
+            print('database update successful')
125
+
126
+        return True
127
+    ## end def
128
+
129
+    def createWeaGraph(self, fileName, dataItem, gLabel, gTitle, gStart,
130
+                        lower, upper, addTrend, autoScale):
131
+        """Uses rrdtool to create a graph of specified weather data item.
132
+           Graphs are for display in html documents.
133
+           Parameters:
134
+               fileName - name of graph file
135
+               dataItem - the weather data item to be graphed
136
+               gLabel - string containing a graph label for the data item
137
+               gTitle - string containing a title for the graph
138
+               gStart - time from now when graph starts
139
+               lower - lower bound for graph ordinate
140
+               upper - upper bound for graph ordinate
141
+               addTrend - 0, show only graph data
142
+                          1, show only a trend line
143
+                          2, show a trend line and the graph data
144
+               autoScale - if True, then use vertical axis auto scaling
145
+                   (lower and upper parameters are ignored), otherwise use
146
+                   lower and upper parameters to set vertical axis scale
147
+           Returns: True if successful, False otherwise
148
+        """
149
+        gPath = self.chartsDirectory + fileName + '.png'
150
+
151
+        # Format the rrdtool graph command.
152
+
153
+        # Set chart start time, height, and width.
154
+        strCmd = 'rrdtool graph %s -a PNG -s %s -e \'now\' -w %s -h %s ' \
155
+                 % (gPath, gStart, self.chartWidth, self.chartHeight)
156
+       
157
+        # Set the range and scaling of the chart y-axis.
158
+        if lower < upper:
159
+            strCmd  +=  '-l %s -u %s -r ' % (lower, upper)
160
+        elif autoScale:
161
+            strCmd += '-A '
162
+        strCmd += '-Y '
163
+
164
+        # Set the chart ordinate label and chart title. 
165
+        strCmd += '-v %s -t %s ' % (gLabel, gTitle)
166
+
167
+        # Show the data, or a moving average trend line, or both.
168
+        strCmd += 'DEF:dSeries=%s:%s:AVERAGE ' % (self.rrdFile, dataItem)
169
+        if addTrend == 0:
170
+            strCmd += 'LINE1:dSeries#0400ff '
171
+        elif addTrend == 1:
172
+            strCmd += 'CDEF:smoothed=dSeries,86400,TREND LINE2:smoothed#006600 '
173
+        elif addTrend == 2:
174
+            strCmd += 'LINE1:dSeries#0400ff '
175
+            strCmd += 'CDEF:smoothed=dSeries,86400,TREND LINE2:smoothed#006600 '
176
+
177
+        # if wind plot show color coded wind direction
178
+        if dataItem == 'windspeedmph':
179
+            strCmd += 'DEF:wDir=%s:winddir:AVERAGE ' % (_RRD_FILE)
180
+            strCmd += 'VDEF:wMax=dSeries,MAXIMUM '
181
+            strCmd += 'CDEF:wMaxScaled=dSeries,0,*,wMax,+,-0.15,* '
182
+            strCmd += 'CDEF:ndir=wDir,337.5,GE,wDir,22.5,LE,+,wMaxScaled,0,IF '
183
+            strCmd += 'CDEF:nedir=wDir,22.5,GT,wDir,67.5,LT,*,wMaxScaled,0,IF '
184
+            strCmd += 'CDEF:edir=wDir,67.5,GE,wDir,112.5,LE,*,wMaxScaled,0,IF '
185
+            strCmd += 'CDEF:sedir=wDir,112.5,GT,wDir,157.5,LT,*,wMaxScaled,0,IF '
186
+            strCmd += 'CDEF:sdir=wDir,157.5,GE,wDir,202.5,LE,*,wMaxScaled,0,IF '
187
+            strCmd += 'CDEF:swdir=wDir,202.5,GT,wDir,247.5,LT,*,wMaxScaled,0,IF '
188
+            strCmd += 'CDEF:wdir=wDir,247.5,GE,wDir,292.5,LE,*,wMaxScaled,0,IF '
189
+            strCmd += 'CDEF:nwdir=wDir,292.5,GT,wDir,337.5,LT,*,wMaxScaled,0,IF '
190
+      
191
+            strCmd += 'AREA:ndir#0000FF:N '    # Blue
192
+            strCmd += 'AREA:nedir#1E90FF:NE '  # DodgerBlue
193
+            strCmd += 'AREA:edir#00FFFF:E '    # Cyan
194
+            strCmd += 'AREA:sedir#00FF00:SE '  # Lime
195
+            strCmd += 'AREA:sdir#FFFF00:S '    # Yellow
196
+            strCmd += 'AREA:swdir#FF8C00:SW '  # DarkOrange 
197
+            strCmd += 'AREA:wdir#FF0000:W '    # Red
198
+            strCmd += 'AREA:nwdir#FF00FF:NW '  # Magenta
199
+        ##end if
200
+        
201
+        if self.debugMode:
202
+            print('%s' % strCmd) # DEBUG
203
+        
204
+        # Run the formatted rrdtool command as a subprocess.
205
+        try:
206
+            result = subprocess.check_output(strCmd, \
207
+                         stderr=subprocess.STDOUT,   \
208
+                         shell=True)
209
+        except subprocess.CalledProcessError as exError:
210
+            print('rrdtool graph failed: %s' % (exError.output.decode('utf-8')))
211
+            return False
212
+
213
+        if self.verboseMode:
214
+            print('rrdtool graph: %s' % result.decode('utf-8')) #, end='')
215
+
216
+        return True
217
+    ## end def
218
+
219
+    def createAutoGraph(self, fileName, dataItem, gLabel, gTitle, gStart,
220
+                    lower, upper, addTrend, autoScale):
221
+        """Uses rrdtool to create a graph of specified radmon data item.
222
+           Parameters:
223
+               fileName - name of file containing the graph
224
+               dataItem - data item to be graphed
225
+               gLabel - string containing a graph label for the data item
226
+               gTitle - string containing a title for the graph
227
+               gStart - beginning time of the graphed data
228
+               lower - lower bound for graph ordinate #NOT USED
229
+               upper - upper bound for graph ordinate #NOT USED
230
+               addTrend - 0, show only graph data
231
+                          1, show only a trend line
232
+                          2, show a trend line and the graph data
233
+               autoScale - if True, then use vertical axis auto scaling
234
+                   (lower and upper parameters are ignored), otherwise use
235
+                   lower and upper parameters to set vertical axis scale
236
+           Returns: True if successful, False otherwise
237
+        """
238
+        gPath = self.chartsDirectory + fileName + ".png"
239
+        trendWindow = { 'end-1day': 7200,
240
+                        'end-4weeks': 172800,
241
+                        'end-12months': 604800 }
242
+     
243
+        # Format the rrdtool graph command.
244
+
245
+        # Set chart start time, height, and width.
246
+        strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
247
+                 % (gPath, gStart, self.chartWidth, self.chartHeight)
248
+       
249
+        # Set the range and scaling of the chart y-axis.
250
+        if lower < upper:
251
+            strCmd  +=  "-l %s -u %s -r " % (lower, upper)
252
+        elif autoScale:
253
+            strCmd += "-A "
254
+        strCmd += "-Y "
255
+
256
+        # Set the chart ordinate label and chart title. 
257
+        strCmd += "-v %s -t %s " % (gLabel, gTitle)
258
+     
259
+        # Show the data, or a moving average trend line over
260
+        # the data, or both.
261
+        strCmd += "DEF:dSeries=%s:%s:LAST " % (self.rrdFile, dataItem)
262
+        if addTrend == 0:
263
+            strCmd += "LINE1:dSeries#0400ff "
264
+        elif addTrend == 1:
265
+            strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE2:smoothed#006600 " \
266
+                      % trendWindow[gStart]
267
+        elif addTrend == 2:
268
+            strCmd += "LINE1:dSeries#0400ff "
269
+            strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE2:smoothed#006600 " \
270
+                      % trendWindow[gStart]
271
+         
272
+        if self.debugMode:
273
+            print("%s" % strCmd) # DEBUG
274
+        
275
+        # Run the formatted rrdtool command as a subprocess.
276
+        try:
277
+            result = subprocess.check_output(strCmd, \
278
+                         stderr=subprocess.STDOUT,   \
279
+                         shell=True)
280
+        except subprocess.CalledProcessError as exError:
281
+            print("rrdtool graph failed: %s" % (exError.output.decode('utf-8')))
282
+            return False
283
+
284
+        if self.verboseMode:
285
+            print("rrdtool graph: %s" % result.decode('utf-8')) #, end='')
286
+        return True
287
+
288
+    ##end def
289
+## end class
290
+