...
|
...
|
@@ -1,18 +1,19 @@
|
1
|
1
|
#!/usr/bin/python -u
|
2
|
|
-## The -u option above turns off block buffering of python output. This assures
|
3
|
|
-## that each error message gets individually printed to the log file.
|
|
2
|
+## The -u option above turns off block buffering of python output. This
|
|
3
|
+## assures that each error message gets individually printed to the log file.
|
4
|
4
|
#
|
5
|
5
|
# Module: radmonAgent.py
|
6
|
6
|
#
|
7
|
|
-# Description: This module acts as an agent between the radiation monitoring device
|
8
|
|
-# and the Internet web server. The agent periodically sends an http request to the
|
9
|
|
-# radiation monitoring device and processes the response from the device and performs
|
10
|
|
-# a number of operations:
|
|
7
|
+# Description: This module acts as an agent between the radiation monitoring
|
|
8
|
+# device and the Internet web server. The agent periodically sends an http
|
|
9
|
+# request to the radiation monitoring device and processes the response from
|
|
10
|
+# the device and performs a number of operations:
|
11
|
11
|
# - conversion of data items
|
12
|
12
|
# - update a round robin (rrdtool) database with the radiation data
|
13
|
13
|
# - periodically generate graphic charts for display in html documents
|
14
|
14
|
# - forward the radiation data to other services
|
15
|
|
-# - write the processed weather data to a JSON file for use by html documents
|
|
15
|
+# - write the processed weather data to a JSON file for use by html
|
|
16
|
+# documents
|
16
|
17
|
#
|
17
|
18
|
# Copyright 2015 Jeff Owrey
|
18
|
19
|
# This program is free software: you can redistribute it and/or modify
|
...
|
...
|
@@ -29,9 +30,12 @@
|
29
|
30
|
# along with this program. If not, see http://www.gnu.org/license.
|
30
|
31
|
#
|
31
|
32
|
# Revision History
|
32
|
|
-# * v20 released 15 Sep 2015 by J L Owrey
|
|
33
|
+# * v20 released 15 Sep 2015 by J L Owrey; first release
|
|
34
|
+# * v21 released 27 Nov 2017 by J L Owrey; bug fixes; updates
|
33
|
35
|
#
|
34
|
36
|
|
|
37
|
+_MIRROR_SERVER = True
|
|
38
|
+
|
35
|
39
|
import os
|
36
|
40
|
import urllib2
|
37
|
41
|
import sys
|
...
|
...
|
@@ -44,32 +48,54 @@ _USER = os.environ['USER']
|
44
|
48
|
|
45
|
49
|
### DEFAULT WEATHER STATION URL ###
|
46
|
50
|
|
47
|
|
-_DEFAULT_RADIATION_MONITOR_URL = "{your weather station url}"
|
48
|
|
-_DATA_FORWARDING_FILE = "/home/%s/public_html/radmon/dynamic/rad.dat" % _USER
|
|
51
|
+if _MIRROR_SERVER:
|
|
52
|
+ _DEFAULT_RADIATION_MONITOR_URL = \
|
|
53
|
+ "http://73.157.139.23:7361/~pi/radmon/dynamic/rad.dat"
|
|
54
|
+else:
|
|
55
|
+ _DEFAULT_RADIATION_MONITOR_URL = "http://192.168.1.8"
|
49
|
56
|
|
50
|
57
|
### FILE AND FOLDER LOCATIONS ###
|
51
|
58
|
|
52
|
|
-_TMP_DIRECTORY = "/tmp/radmon" # folder for charts and output data file
|
53
|
|
-_RRD_FILE = "/home/%s/database/radmonData.rrd" % _USER # database that stores the data
|
54
|
|
-_OUTPUT_DATA_FILE = "/tmp/radmon/radmonData.js" # output file used by HTML docs
|
|
59
|
+# folder for containing dynamic data objects
|
|
60
|
+_DYNAMIC_FOLDER_PATH = "/home/%s/public_html/radmon/dynamic/" % _USER
|
|
61
|
+# folder for charts and output data file
|
|
62
|
+_CHARTS_DIRECTORY = _DYNAMIC_FOLDER_PATH
|
|
63
|
+ # database that stores weather data
|
|
64
|
+_RRD_FILE = "/home/%s/database/radmonData.rrd" % _USER
|
|
65
|
+# location of data output file
|
|
66
|
+_OUTPUT_DATA_FILE = _DYNAMIC_FOLDER_PATH + "radmonData.js"
|
|
67
|
+# location of data forwarding file
|
|
68
|
+_DATA_FORWARDING_FILE = _DYNAMIC_FOLDER_PATH + "rad.dat"
|
55
|
69
|
|
56
|
70
|
### GLOBAL CONSTANTS ###
|
57
|
|
-
|
58
|
|
-_DEFAULT_DATA_REQUEST_INTERVAL = 10 # interval between data requests to radiation monitor
|
59
|
|
-_CHART_UPDATE_INTERVAL = 300 # defines how often the charts get updated in seconds
|
60
|
|
-_DATABASE_UPDATE_INTERVAL = 30 # defines how often the database gets updated
|
61
|
|
-_HTTP_REQUEST_TIMEOUT = 3 # number seconds to wait for a response to HTTP request
|
62
|
|
-_MAX_RADIATION_MONITOR_OFFLINE_COUNT = 2 # max number of failed data requests allowed
|
|
71
|
+# interval in seconds between data requests to radiation monitor
|
|
72
|
+_DEFAULT_DATA_REQUEST_INTERVAL = 5
|
|
73
|
+# defines how often the charts get updated in seconds
|
|
74
|
+_CHART_UPDATE_INTERVAL = 300
|
|
75
|
+# defines how often the database gets updated
|
|
76
|
+_DATABASE_UPDATE_INTERVAL = 30
|
|
77
|
+# number seconds to wait for a response to HTTP request
|
|
78
|
+_HTTP_REQUEST_TIMEOUT = 3
|
|
79
|
+# max number of failed data requests allowed
|
|
80
|
+_MAX_RADIATION_MONITOR_OFFLINE_COUNT = 2
|
|
81
|
+# radmon chart dimensions
|
63
|
82
|
_CHART_WIDTH = 600
|
64
|
83
|
_CHART_HEIGHT = 150
|
65
|
84
|
|
66
|
85
|
### GLOBAL VARIABLES ###
|
67
|
86
|
|
|
87
|
+# turn on or off of verbose debugging information
|
68
|
88
|
debugOption = False
|
|
89
|
+# online status of radiation monitor
|
69
|
90
|
radiationMonitorOnline = True
|
|
91
|
+# number of unsuccessful http requests
|
70
|
92
|
radiationMonitorOfflineCount = 0
|
71
|
|
-dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL # web update frequency
|
72
|
|
-radiationMonitorUrl = _DEFAULT_RADIATION_MONITOR_URL # radiation monitor network address
|
|
93
|
+# status of reset command to radiation monitor
|
|
94
|
+remoteDeviceReset = False
|
|
95
|
+# web update frequency
|
|
96
|
+dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
|
|
97
|
+# radiation monitor network address
|
|
98
|
+radiationMonitorUrl = _DEFAULT_RADIATION_MONITOR_URL
|
73
|
99
|
|
74
|
100
|
|
75
|
101
|
### PRIVATE METHODS ###
|
...
|
...
|
@@ -101,14 +127,13 @@ def setOfflineStatus(dData):
|
101
|
127
|
# that we are now offline.
|
102
|
128
|
if radiationMonitorOnline:
|
103
|
129
|
print "%s: radiation monitor offline" % getTimeStamp()
|
|
130
|
+ if os.path.exists(_DATA_FORWARDING_FILE):
|
|
131
|
+ os.remove(_DATA_FORWARDING_FILE)
|
104
|
132
|
radiationMonitorOnline = False
|
105
|
133
|
|
106
|
|
- # Set data items to blank.
|
107
|
|
- dData['UTC'] = ''
|
108
|
|
- dData['CPM'] = ''
|
109
|
|
- dData['CPS'] = ''
|
110
|
|
- dData['uSvPerHr'] = ''
|
111
|
|
- dData['Mode'] = ''
|
|
134
|
+ for key in dData:
|
|
135
|
+ dData[key] = ''
|
|
136
|
+
|
112
|
137
|
dData['status'] = 'offline'
|
113
|
138
|
|
114
|
139
|
writeOutputDataFile(dData)
|
...
|
...
|
@@ -128,11 +153,20 @@ def getRadiationData():
|
128
|
153
|
Returns a string containing the radiation data, or None if
|
129
|
154
|
not successful.
|
130
|
155
|
"""
|
131
|
|
- global radiationMonitorOnline, radiationMonitorOfflineCount
|
|
156
|
+ global radiationMonitorOnline, radiationMonitorOfflineCount, \
|
|
157
|
+ remoteDeviceReset
|
|
158
|
+
|
|
159
|
+ sUrl = radiationMonitorUrl
|
|
160
|
+
|
|
161
|
+ if not _MIRROR_SERVER:
|
|
162
|
+ if remoteDeviceReset:
|
|
163
|
+ sUrl += "/reset"
|
|
164
|
+ remoteDeviceReset = False
|
|
165
|
+ else:
|
|
166
|
+ sUrl += "/rdata"
|
132
|
167
|
|
133
|
168
|
try:
|
134
|
|
- conn = urllib2.urlopen(radiationMonitorUrl,
|
135
|
|
- timeout=_HTTP_REQUEST_TIMEOUT)
|
|
169
|
+ conn = urllib2.urlopen(sUrl, timeout=_HTTP_REQUEST_TIMEOUT)
|
136
|
170
|
|
137
|
171
|
# Format received data into a single string.
|
138
|
172
|
content = ""
|
...
|
...
|
@@ -197,13 +231,26 @@ def convertData(dData):
|
197
|
231
|
result = True
|
198
|
232
|
|
199
|
233
|
try:
|
200
|
|
- # Convert UTC from radiation monitoring device to local time.
|
|
234
|
+
|
|
235
|
+ # Uncomment below to use timestamp from radiation monitoring device
|
|
236
|
+ # otherwise the requesting server (this) will generate the
|
|
237
|
+ # timestamp. Allowing the server to generate the timestamp
|
|
238
|
+ # prevents timestamp errors due to the radiation monitoring device
|
|
239
|
+ # failing to synchronize with a NTP time server.
|
|
240
|
+
|
|
241
|
+ #dData['UTC'] = time.time()
|
|
242
|
+
|
|
243
|
+ ## Convert UTC from radiation monitoring device to local time.
|
201
|
244
|
ts_utc = time.strptime(dData['UTC'], "%H:%M:%S %m/%d/%Y")
|
202
|
245
|
local_sec = calendar.timegm(ts_utc)
|
203
|
246
|
dData['UTC'] = local_sec
|
204
|
|
-
|
|
247
|
+
|
205
|
248
|
dData['Mode'] = dData['Mode'].lower()
|
206
|
|
- dData['uSvPerHr'] = dData.pop('uSv/hr')
|
|
249
|
+
|
|
250
|
+ dData['uSvPerHr'] = float(dData.pop('uSv/hr'))
|
|
251
|
+
|
|
252
|
+ dData['CPM'] = int(dData.pop('CPM'))
|
|
253
|
+
|
207
|
254
|
except Exception, exError:
|
208
|
255
|
print "%s convertData: %s" % (getTimeStamp(), exError)
|
209
|
256
|
result = False
|
...
|
...
|
@@ -243,9 +290,6 @@ def writeOutputDataFile(dData):
|
243
|
290
|
## end def
|
244
|
291
|
|
245
|
292
|
def writeForwardingFile(sData):
|
246
|
|
- """Write weather station response string to a forwarding file for use
|
247
|
|
- by down stream servers that mirror this site.
|
248
|
|
- """
|
249
|
293
|
# Write the string to the output data file for use by html documents.
|
250
|
294
|
try:
|
251
|
295
|
fc = open(_DATA_FORWARDING_FILE, "w")
|
...
|
...
|
@@ -267,8 +311,11 @@ def updateDatabase(dData):
|
267
|
311
|
written to the rr database file
|
268
|
312
|
Returns true if successful, false otherwise.
|
269
|
313
|
"""
|
270
|
|
- # The RR database stores whole units, so convert uSv to Sv.
|
271
|
|
- Svvalue = float(dData['uSvPerHr']) * 1.0E-06 # convert micro-Sieverts to Sieverts
|
|
314
|
+ global remoteDeviceReset
|
|
315
|
+
|
|
316
|
+ # The RR database stores whole units, so convert uSv to Sv.
|
|
317
|
+ #Svvalue = float(dData['uSvPerHr']) * 1.0E-06
|
|
318
|
+ Svvalue = dData['uSvPerHr'] * 1.0E-06
|
272
|
319
|
|
273
|
320
|
# Create the rrdtool update command.
|
274
|
321
|
strCmd = "rrdtool update %s %s:%s:%s" % \
|
...
|
...
|
@@ -283,6 +330,11 @@ def updateDatabase(dData):
|
283
|
330
|
except subprocess.CalledProcessError, exError:
|
284
|
331
|
print "%s: rrdtool update failed: %s" % \
|
285
|
332
|
(getTimeStamp(), exError.output)
|
|
333
|
+ if exError.output.find("illegal attempt to update using time") > -1:
|
|
334
|
+ remoteDeviceReset = True
|
|
335
|
+ print "%s: rebooting radiation monitor" % \
|
|
336
|
+ (getTimeStamp())
|
|
337
|
+
|
286
|
338
|
return False
|
287
|
339
|
|
288
|
340
|
return True
|
...
|
...
|
@@ -306,7 +358,7 @@ def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
|
306
|
358
|
lower and upper parameters to set vertical axis scale
|
307
|
359
|
Returns true if successful, false otherwise.
|
308
|
360
|
"""
|
309
|
|
- gPath = _TMP_DIRECTORY + '/' + fileName + ".png"
|
|
361
|
+ gPath = _CHARTS_DIRECTORY + fileName + ".png"
|
310
|
362
|
trendWindow = { 'end-1day': 7200,
|
311
|
363
|
'end-4weeks': 172800,
|
312
|
364
|
'end-12months': 604800 }
|
...
|
...
|
@@ -410,24 +462,25 @@ def getCLarguments():
|
410
|
462
|
##end def
|
411
|
463
|
|
412
|
464
|
def main():
|
413
|
|
- """Handles timing of events and acts as executive routine managing all other
|
414
|
|
- functions.
|
|
465
|
+ """Handles timing of events and acts as executive routine managing
|
|
466
|
+ all other functions.
|
415
|
467
|
Parameters: none
|
416
|
468
|
Returns nothing.
|
417
|
469
|
"""
|
418
|
470
|
|
419
|
|
- lastDataRequestTime = -1 # last time output JSON file updated
|
420
|
|
- lastChartUpdateTime = - 1 # last time charts generated
|
421
|
|
- lastDatabaseUpdateTime = -1 # last time the rrdtool database updated
|
422
|
|
- dData = {} # dictionary object for temporary data storage
|
|
471
|
+ # last time output JSON file updated
|
|
472
|
+ lastDataRequestTime = -1
|
|
473
|
+ # last time charts generated
|
|
474
|
+ lastChartUpdateTime = - 1
|
|
475
|
+ # last time the rrdtool database updated
|
|
476
|
+ lastDatabaseUpdateTime = -1
|
|
477
|
+
|
|
478
|
+ # define empty dictionary object for radmon data
|
|
479
|
+ dData = {}
|
423
|
480
|
|
424
|
481
|
## Get command line arguments.
|
425
|
482
|
getCLarguments()
|
426
|
483
|
|
427
|
|
- ## Create www data folder if it does not already exist.
|
428
|
|
- if not os.path.isdir(_TMP_DIRECTORY):
|
429
|
|
- os.makedirs(_TMP_DIRECTORY)
|
430
|
|
-
|
431
|
484
|
## Exit with error if rrdtool database does not exist.
|
432
|
485
|
if not os.path.exists(_RRD_FILE):
|
433
|
486
|
print "cannot find rrdtool database\nuse createWeatherRrd script to" \
|
...
|
...
|
@@ -453,6 +506,7 @@ def main():
|
453
|
506
|
|
454
|
507
|
# If successful parse the data.
|
455
|
508
|
if result:
|
|
509
|
+ dData = {}
|
456
|
510
|
result = parseDataString(sData, dData)
|
457
|
511
|
|
458
|
512
|
# If parsing successful, convert the data.
|
...
|
...
|
@@ -467,7 +521,8 @@ def main():
|
467
|
521
|
print "http request successful"
|
468
|
522
|
|
469
|
523
|
# At the rrdtool database update interval, update the database.
|
470
|
|
- if currentTime - lastDatabaseUpdateTime > _DATABASE_UPDATE_INTERVAL:
|
|
524
|
+ if currentTime - lastDatabaseUpdateTime > \
|
|
525
|
+ _DATABASE_UPDATE_INTERVAL:
|
471
|
526
|
lastDatabaseUpdateTime = currentTime
|
472
|
527
|
## Update the round robin database with the parsed data.
|
473
|
528
|
result = updateDatabase(dData)
|