... | ... |
@@ -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 |
+ |