... | ... |
@@ -49,8 +49,19 @@ import json |
49 | 49 |
import ina260 # power sensor |
50 | 50 |
import tmp102 # temperature sensor |
51 | 51 |
|
52 |
+# Import custom libraries |
|
53 |
+import smsalert |
|
54 |
+ |
|
52 | 55 |
### ENVIRONMENT ### |
56 |
+ |
|
53 | 57 |
_USER = os.environ['USER'] |
58 |
+_HOSTNAME = os.uname()[1] |
|
59 |
+ |
|
60 |
+ ### SMS RECIPIENTS ### |
|
61 |
+ |
|
62 |
+_SMS_CALLSIGN = '{your callsign}' |
|
63 |
+_SMS_PASSCODE = '{your passcode}' |
|
64 |
+_SMS_PHONE_NUMBER = '{your phone number}' |
|
54 | 65 |
|
55 | 66 |
### SENSOR BUS ADDRESSES ### |
56 | 67 |
|
... | ... |
@@ -89,11 +100,16 @@ _CHART_WIDTH = 600 |
89 | 100 |
_CHART_HEIGHT = 150 |
90 | 101 |
# chart average line color |
91 | 102 |
_AVERAGE_LINE_COLOR = '#006600' |
103 |
+# low voltage alert threshold |
|
104 |
+_DEFAULT_CRITICAL_LOW_VOLTAGE = 12.0 |
|
92 | 105 |
|
93 | 106 |
### GLOBAL VARIABLES ### |
94 | 107 |
|
95 |
-# Container for sensor objects. |
|
96 |
-dSensors = {} |
|
108 |
+# Sensor instance objects. |
|
109 |
+power1 = None |
|
110 |
+battemp = None |
|
111 |
+ambtemp = None |
|
112 |
+sms = None |
|
97 | 113 |
|
98 | 114 |
# turns on or off extensive debugging messages |
99 | 115 |
debugMode = False |
... | ... |
@@ -107,6 +123,8 @@ chartUpdateInterval = _CHART_UPDATE_INTERVAL |
107 | 123 |
failedUpdateCount = 0 |
108 | 124 |
# sensor status |
109 | 125 |
deviceOnline = False |
126 |
+# sms message sent status |
|
127 |
+bSMSmsgSent = False |
|
110 | 128 |
|
111 | 129 |
### PRIVATE METHODS ### |
112 | 130 |
|
... | ... |
@@ -182,11 +200,11 @@ def getSensorData(dData): |
182 | 200 |
dData["time"] = getTimeStamp() |
183 | 201 |
|
184 | 202 |
try: |
185 |
- dData["current"] = dSensors['power'].getCurrent() |
|
186 |
- dData["voltage"] = dSensors['power'].getVoltage() |
|
187 |
- dData["power"] = dSensors['power'].getPower() |
|
188 |
- dData["battemp"] = dSensors['battemp'].getTempF() |
|
189 |
- dData["ambtemp"] = dSensors['ambtemp'].getTempF() |
|
203 |
+ dData["current"] = power1.getCurrent() |
|
204 |
+ dData["voltage"] = power1.getVoltage() |
|
205 |
+ dData["power"] = power1.getPower() |
|
206 |
+ dData["battemp"] = battemp.getTempF() |
|
207 |
+ dData["ambtemp"] = ambtemp.getTempF() |
|
190 | 208 |
except Exception as exError: |
191 | 209 |
print("%s sensor error: %s" % (getTimeStamp(), exError)) |
192 | 210 |
return False |
... | ... |
@@ -196,6 +214,34 @@ def getSensorData(dData): |
196 | 214 |
return True |
197 | 215 |
## end def |
198 | 216 |
|
217 |
+def convertData(dData): |
|
218 |
+ """ |
|
219 |
+ Converts data items and verifies threshold crossings. |
|
220 |
+ Parameters: dData - a dictionary object that contains the sensor data |
|
221 |
+ Returns: True if successful, False otherwise |
|
222 |
+ """ |
|
223 |
+ global bSMSmsgSent |
|
224 |
+ |
|
225 |
+ if not bSMSmsgSent and dData["voltage"] <= _DEFAULT_CRITICAL_LOW_VOLTAGE: |
|
226 |
+ # Format a text alert message. |
|
227 |
+ message = "%s %s low voltage alert: %s volts" % \ |
|
228 |
+ (getTimeStamp(), _HOSTNAME, dData["voltage"]) |
|
229 |
+ print(message) |
|
230 |
+ # Send the text alert to recipient phone numbers. |
|
231 |
+ sms.sendSMS(_SMS_PHONE_NUMBER, message) |
|
232 |
+ bSMSmsgSent = True |
|
233 |
+ elif bSMSmsgSent and dData["voltage"] > _DEFAULT_CRITICAL_LOW_VOLTAGE: |
|
234 |
+ # Format a text alert message. |
|
235 |
+ message = "%s %s voltage normal: %s volts" % \ |
|
236 |
+ (getTimeStamp(), _HOSTNAME, dData["voltage"]) |
|
237 |
+ print(message) |
|
238 |
+ # Send the text alert to recipient phone numbers. |
|
239 |
+ sms.sendSMS(_SMS_PHONE_NUMBER, message) |
|
240 |
+ bSMSmsgSent = False |
|
241 |
+ ## end if |
|
242 |
+ return True |
|
243 |
+## end def |
|
244 |
+ |
|
199 | 245 |
def writeOutputFile(dData): |
200 | 246 |
""" |
201 | 247 |
Write sensor data items to the output data file, formatted as |
... | ... |
@@ -492,13 +538,15 @@ def getCLarguments(): |
492 | 538 |
index += 1 |
493 | 539 |
##end def |
494 | 540 |
|
495 |
-def main(): |
|
541 |
+def main_setup(): |
|
496 | 542 |
""" |
497 | 543 |
Handles timing of events and acts as executive routine managing |
498 | 544 |
all other functions. |
499 | 545 |
Parameters: none |
500 | 546 |
Returns: nothing |
501 | 547 |
""" |
548 |
+ global power1, battemp, ambtemp, sms |
|
549 |
+ |
|
502 | 550 |
signal.signal(signal.SIGTERM, terminateAgentProcess) |
503 | 551 |
signal.signal(signal.SIGINT, terminateAgentProcess) |
504 | 552 |
|
... | ... |
@@ -518,15 +566,15 @@ def main(): |
518 | 566 |
exit(1) |
519 | 567 |
|
520 | 568 |
# Create sensor objects. This also initializes each sensor. |
521 |
- dSensors['power'] = ina260.ina260(_PWR_SENSOR_ADDR, _BUS_NUMBER, |
|
569 |
+ power1 = ina260.ina260(_PWR_SENSOR_ADDR, _BUS_NUMBER, |
|
522 | 570 |
debug=debugMode) |
523 |
- dSensors['battemp'] = tmp102.tmp102(_BAT_TMP_SENSOR_ADDR, _BUS_NUMBER, |
|
571 |
+ battemp = tmp102.tmp102(_BAT_TMP_SENSOR_ADDR, _BUS_NUMBER, |
|
524 | 572 |
debug=debugMode) |
525 |
- dSensors['ambtemp'] = tmp102.tmp102(_AMB_TMP_SENSOR_ADDR, _BUS_NUMBER, |
|
573 |
+ ambtemp = tmp102.tmp102(_AMB_TMP_SENSOR_ADDR, _BUS_NUMBER, |
|
526 | 574 |
debug=debugMode) |
575 |
+ # Create instance of SMS alert class |
|
576 |
+ sms = smsalert.smsalert(_SMS_CALLSIGN, _SMS_PASSCODE, debug=debugMode) |
|
527 | 577 |
|
528 |
- # Enter main loop. |
|
529 |
- main_loop() |
|
530 | 578 |
## end def |
531 | 579 |
|
532 | 580 |
def main_loop(): |
... | ... |
@@ -550,9 +598,13 @@ def main_loop(): |
550 | 598 |
dData = {} |
551 | 599 |
|
552 | 600 |
# Get the data from the sensors. |
553 |
- result =getSensorData(dData) |
|
601 |
+ result = getSensorData(dData) |
|
554 | 602 |
|
555 |
- # If get data successful, write data to data files. |
|
603 |
+ # If get data successful, convert the data and verify thresholds. |
|
604 |
+ if result: |
|
605 |
+ result = convertData(dData) |
|
606 |
+ |
|
607 |
+ # If convert data successful, write data to data files. |
|
556 | 608 |
if result: |
557 | 609 |
result = writeOutputFile(dData) |
558 | 610 |
|
... | ... |
@@ -589,4 +641,5 @@ def main_loop(): |
589 | 641 |
## end def |
590 | 642 |
|
591 | 643 |
if __name__ == '__main__': |
592 |
- main() |
|
644 |
+ main_setup() |
|
645 |
+ main_loop() |
... | ... |
@@ -92,6 +92,9 @@ _AVERAGE_LINE_COLOR = '#006600' |
92 | 92 |
|
93 | 93 |
### GLOBAL VARIABLES ### |
94 | 94 |
|
95 |
+# Container for sensor objects. |
|
96 |
+dSensors = {} |
|
97 |
+ |
|
95 | 98 |
# turns on or off extensive debugging messages |
96 | 99 |
debugMode = False |
97 | 100 |
verboseMode = False |
... | ... |
@@ -167,7 +170,7 @@ def terminateAgentProcess(signal, frame): |
167 | 170 |
|
168 | 171 |
### PUBLIC METHODS ### |
169 | 172 |
|
170 |
-def getSensorData(dSensors, dData): |
|
173 |
+def getSensorData(dData): |
|
171 | 174 |
""" |
172 | 175 |
Poll sensors for data. Store the data in a dictionary object for |
173 | 176 |
use by other subroutines. The dictionary object passed in should |
... | ... |
@@ -504,13 +507,6 @@ def main(): |
504 | 507 |
'%s starting up node power agent process' % \ |
505 | 508 |
(getTimeStamp()) |
506 | 509 |
|
507 |
- # last time output JSON file updated |
|
508 |
- lastDataRequestTime = -1 |
|
509 |
- # last time charts generated |
|
510 |
- lastChartUpdateTime = - 1 |
|
511 |
- # last time the rrdtool database updated |
|
512 |
- lastDatabaseUpdateTime = -1 |
|
513 |
- |
|
514 | 510 |
## Get command line arguments. |
515 | 511 |
getCLarguments() |
516 | 512 |
|
... | ... |
@@ -522,7 +518,6 @@ def main(): |
522 | 518 |
exit(1) |
523 | 519 |
|
524 | 520 |
# Create sensor objects. This also initializes each sensor. |
525 |
- dSensors = {} |
|
526 | 521 |
dSensors['power'] = ina260.ina260(_PWR_SENSOR_ADDR, _BUS_NUMBER, |
527 | 522 |
debug=debugMode) |
528 | 523 |
dSensors['battemp'] = tmp102.tmp102(_BAT_TMP_SENSOR_ADDR, _BUS_NUMBER, |
... | ... |
@@ -530,6 +525,18 @@ def main(): |
530 | 525 |
dSensors['ambtemp'] = tmp102.tmp102(_AMB_TMP_SENSOR_ADDR, _BUS_NUMBER, |
531 | 526 |
debug=debugMode) |
532 | 527 |
|
528 |
+ # Enter main loop. |
|
529 |
+ main_loop() |
|
530 |
+## end def |
|
531 |
+ |
|
532 |
+def main_loop(): |
|
533 |
+ # last time output JSON file updated |
|
534 |
+ lastDataRequestTime = -1 |
|
535 |
+ # last time charts generated |
|
536 |
+ lastChartUpdateTime = - 1 |
|
537 |
+ # last time the rrdtool database updated |
|
538 |
+ lastDatabaseUpdateTime = -1 |
|
539 |
+ |
|
533 | 540 |
### MAIN LOOP ### |
534 | 541 |
|
535 | 542 |
while True: |
... | ... |
@@ -543,7 +550,7 @@ def main(): |
543 | 550 |
dData = {} |
544 | 551 |
|
545 | 552 |
# Get the data from the sensors. |
546 |
- result =getSensorData(dSensors, dData) |
|
553 |
+ result =getSensorData(dData) |
|
547 | 554 |
|
548 | 555 |
# If get data successful, write data to data files. |
549 | 556 |
if result: |
... | ... |
@@ -579,7 +586,6 @@ def main(): |
579 | 586 |
if remainingTime > 0.0: |
580 | 587 |
time.sleep(remainingTime) |
581 | 588 |
## end while |
582 |
- return |
|
583 | 589 |
## end def |
584 | 590 |
|
585 | 591 |
if __name__ == '__main__': |
... | ... |
@@ -188,6 +188,8 @@ def getSensorData(dSensors, dData): |
188 | 188 |
print("%s sensor error: %s" % (getTimeStamp(), exError)) |
189 | 189 |
return False |
190 | 190 |
|
191 |
+ dData['chartUpdateInterval'] = chartUpdateInterval |
|
192 |
+ |
|
191 | 193 |
return True |
192 | 194 |
## end def |
193 | 195 |
|
... | ... |
@@ -212,7 +214,6 @@ def writeOutputFile(dData): |
212 | 214 |
try: |
213 | 215 |
for key in dData: |
214 | 216 |
jsData.update({key:dData[key]}) |
215 |
- jsData.update({"chartUpdateInterval": chartUpdateInterval}) |
|
216 | 217 |
sData = "[%s]" % json.dumps(jsData) |
217 | 218 |
except Exception as exError: |
218 | 219 |
print("%s writeOutputFile: %s" % (getTimeStamp(), exError)) |
... | ... |
@@ -253,17 +254,20 @@ def setStatus(updateSuccess): |
253 | 254 |
if not deviceOnline: |
254 | 255 |
print('%s device online' % getTimeStamp()) |
255 | 256 |
deviceOnline = True |
257 |
+ return |
|
256 | 258 |
else: |
257 | 259 |
# The last attempt failed, so update the failed attempts |
258 | 260 |
# count. |
259 | 261 |
failedUpdateCount += 1 |
260 | 262 |
|
261 |
- if failedUpdateCount >= _MAX_FAILED_DATA_REQUESTS: |
|
263 |
+ if failedUpdateCount == _MAX_FAILED_DATA_REQUESTS: |
|
262 | 264 |
# Max number of failed data requests, so set |
263 | 265 |
# device status to offline. |
264 | 266 |
setStatusToOffline() |
265 | 267 |
##end def |
266 | 268 |
|
269 |
+ ### DATABASE FUNCTIONS ### |
|
270 |
+ |
|
267 | 271 |
def updateDatabase(dData): |
268 | 272 |
""" |
269 | 273 |
Update the rrdtool database by executing an rrdtool system command. |
... | ... |
@@ -289,12 +293,12 @@ def updateDatabase(dData): |
289 | 293 |
subprocess.check_output(strCmd, shell=True, \ |
290 | 294 |
stderr=subprocess.STDOUT) |
291 | 295 |
except subprocess.CalledProcessError as exError: |
292 |
- print("%s: rrdtool update failed: %s" % \ |
|
296 |
+ print("%s: rrdtool update: %s" % \ |
|
293 | 297 |
(getTimeStamp(), exError.output)) |
294 | 298 |
return False |
295 | 299 |
|
296 | 300 |
if verboseMode and not debugMode: |
297 |
- print("database updated") |
|
301 |
+ print("database update successful") |
|
298 | 302 |
|
299 | 303 |
return True |
300 | 304 |
## end def |
... | ... |
@@ -373,7 +377,8 @@ def createGraph(fileName, dataItem, gLabel, gTitle, gStart, |
373 | 377 |
stderr=subprocess.STDOUT, \ |
374 | 378 |
shell=True) |
375 | 379 |
except subprocess.CalledProcessError as exError: |
376 |
- print("rrdtool graph failed: %s" % (exError.output)) |
|
380 |
+ print("%s rrdtool graph: %s" % \ |
|
381 |
+ (getTimeStamp(), exError.output)) |
|
377 | 382 |
return False |
378 | 383 |
|
379 | 384 |
if verboseMode and not debugMode: |
... | ... |
@@ -31,6 +31,8 @@ |
31 | 31 |
# * v10 released 01 June 2021 by J L Owrey; first release |
32 | 32 |
# * v11 released 02 July 2021 by J L Owrey; improved sensor fault |
33 | 33 |
# handling; improved code readability |
34 |
+# * v12 released 06 July 2021 by J L Owrey; improved debug mode |
|
35 |
+# handling; debug mode state now passed to sensor object constructors |
|
34 | 36 |
# |
35 | 37 |
#2345678901234567890123456789012345678901234567890123456789012345678901234567890 |
36 | 38 |
|
... | ... |
@@ -103,11 +105,6 @@ failedUpdateCount = 0 |
103 | 105 |
# sensor status |
104 | 106 |
deviceOnline = False |
105 | 107 |
|
106 |
-# Create sensor objects. This also initialzes each sensor. |
|
107 |
-power = ina260.ina260(_PWR_SENSOR_ADDR, _BUS_NUMBER) |
|
108 |
-battemp = tmp102.tmp102(_BAT_TMP_SENSOR_ADDR, _BUS_NUMBER) |
|
109 |
-ambtemp = tmp102.tmp102(_AMB_TMP_SENSOR_ADDR, _BUS_NUMBER) |
|
110 |
- |
|
111 | 108 |
### PRIVATE METHODS ### |
112 | 109 |
|
113 | 110 |
def getTimeStamp(): |
... | ... |
@@ -170,22 +167,23 @@ def terminateAgentProcess(signal, frame): |
170 | 167 |
|
171 | 168 |
### PUBLIC METHODS ### |
172 | 169 |
|
173 |
-def getSensorData(dData): |
|
170 |
+def getSensorData(dSensors, dData): |
|
174 | 171 |
""" |
175 | 172 |
Poll sensors for data. Store the data in a dictionary object for |
176 | 173 |
use by other subroutines. The dictionary object passed in should |
177 | 174 |
an empty dictionary, i.e., dData = { }. |
178 | 175 |
Parameters: dData - a dictionary object to contain the sensor data |
176 |
+ dSensors - a dictionary containing sensor objects |
|
179 | 177 |
Returns: True if successful, False otherwise |
180 | 178 |
""" |
181 | 179 |
dData["time"] = getTimeStamp() |
182 | 180 |
|
183 | 181 |
try: |
184 |
- dData["current"] = power.getCurrent() |
|
185 |
- dData["voltage"] = power.getVoltage() |
|
186 |
- dData["power"] = power.getPower() |
|
187 |
- dData["battemp"] = battemp.getTempF() |
|
188 |
- dData["ambtemp"] = ambtemp.getTempF() |
|
182 |
+ dData["current"] = dSensors['power'].getCurrent() |
|
183 |
+ dData["voltage"] = dSensors['power'].getVoltage() |
|
184 |
+ dData["power"] = dSensors['power'].getPower() |
|
185 |
+ dData["battemp"] = dSensors['battemp'].getTempF() |
|
186 |
+ dData["ambtemp"] = dSensors['ambtemp'].getTempF() |
|
189 | 187 |
except Exception as exError: |
190 | 188 |
print("%s sensor error: %s" % (getTimeStamp(), exError)) |
191 | 189 |
return False |
... | ... |
@@ -517,8 +515,18 @@ def main(): |
517 | 515 |
'use createPowerRrd script to ' \ |
518 | 516 |
'create rrdtool database\n') |
519 | 517 |
exit(1) |
520 |
- |
|
521 |
- ## main loop |
|
518 |
+ |
|
519 |
+ # Create sensor objects. This also initializes each sensor. |
|
520 |
+ dSensors = {} |
|
521 |
+ dSensors['power'] = ina260.ina260(_PWR_SENSOR_ADDR, _BUS_NUMBER, |
|
522 |
+ debug=debugMode) |
|
523 |
+ dSensors['battemp'] = tmp102.tmp102(_BAT_TMP_SENSOR_ADDR, _BUS_NUMBER, |
|
524 |
+ debug=debugMode) |
|
525 |
+ dSensors['ambtemp'] = tmp102.tmp102(_AMB_TMP_SENSOR_ADDR, _BUS_NUMBER, |
|
526 |
+ debug=debugMode) |
|
527 |
+ |
|
528 |
+ ### MAIN LOOP ### |
|
529 |
+ |
|
522 | 530 |
while True: |
523 | 531 |
|
524 | 532 |
currentTime = time.time() # get current time in seconds |
... | ... |
@@ -530,7 +538,7 @@ def main(): |
530 | 538 |
dData = {} |
531 | 539 |
|
532 | 540 |
# Get the data from the sensors. |
533 |
- result =getSensorData(dData) |
|
541 |
+ result =getSensorData(dSensors, dData) |
|
534 | 542 |
|
535 | 543 |
# If get data successful, write data to data files. |
536 | 544 |
if result: |
... | ... |
@@ -29,6 +29,8 @@ |
29 | 29 |
# |
30 | 30 |
# Revision History |
31 | 31 |
# * v10 released 01 June 2021 by J L Owrey; first release |
32 |
+# * v11 released 02 July 2021 by J L Owrey; improved sensor fault |
|
33 |
+# handling; improved code readability |
|
32 | 34 |
# |
33 | 35 |
#2345678901234567890123456789012345678901234567890123456789012345678901234567890 |
34 | 36 |
|
... | ... |
@@ -55,7 +57,7 @@ _PWR_SENSOR_ADDR = 0X40 |
55 | 57 |
_BAT_TMP_SENSOR_ADDR = 0x48 |
56 | 58 |
_AMB_TMP_SENSOR_ADDR = 0x4B |
57 | 59 |
# Set bus selector. |
58 |
-_BUS_SEL = 1 |
|
60 |
+_BUS_NUMBER = 1 |
|
59 | 61 |
|
60 | 62 |
### FILE AND FOLDER LOCATIONS ### |
61 | 63 |
|
... | ... |
@@ -71,9 +73,12 @@ _RRD_FILE = "/home/%s/database/powerData.rrd" % _USER |
71 | 73 |
### GLOBAL CONSTANTS ### |
72 | 74 |
|
73 | 75 |
# sensor data request interval in seconds |
74 |
-_DEFAULT_DATA_REQUEST_INTERVAL = 2 |
|
76 |
+_DEFAULT_SENSOR_POLLING_INTERVAL = 2 |
|
75 | 77 |
# rrdtool database update interval in seconds |
76 | 78 |
_DATABASE_UPDATE_INTERVAL = 30 |
79 |
+# max number of failed attempts to get sensor data |
|
80 |
+_MAX_FAILED_DATA_REQUESTS = 2 |
|
81 |
+ |
|
77 | 82 |
# chart update interval in seconds |
78 | 83 |
_CHART_UPDATE_INTERVAL = 600 |
79 | 84 |
# standard chart width in pixels |
... | ... |
@@ -85,19 +90,23 @@ _AVERAGE_LINE_COLOR = '#006600' |
85 | 90 |
|
86 | 91 |
### GLOBAL VARIABLES ### |
87 | 92 |
|
88 |
-# debug output options |
|
93 |
+# turns on or off extensive debugging messages |
|
89 | 94 |
debugMode = False |
90 | 95 |
verboseMode = False |
91 | 96 |
|
92 | 97 |
# frequency of data requests to sensors |
93 |
-dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL |
|
98 |
+dataRequestInterval = _DEFAULT_SENSOR_POLLING_INTERVAL |
|
94 | 99 |
# how often charts get updated |
95 | 100 |
chartUpdateInterval = _CHART_UPDATE_INTERVAL |
101 |
+# number of failed attempts to get sensor data |
|
102 |
+failedUpdateCount = 0 |
|
103 |
+# sensor status |
|
104 |
+deviceOnline = False |
|
96 | 105 |
|
97 |
-# Define each sensor. This also initialzes each sensor. |
|
98 |
-pwr = ina260.ina260(_PWR_SENSOR_ADDR, _BUS_SEL) |
|
99 |
-btmp = tmp102.tmp102(_BAT_TMP_SENSOR_ADDR, _BUS_SEL) |
|
100 |
-atmp = tmp102.tmp102(_AMB_TMP_SENSOR_ADDR, _BUS_SEL) |
|
106 |
+# Create sensor objects. This also initialzes each sensor. |
|
107 |
+power = ina260.ina260(_PWR_SENSOR_ADDR, _BUS_NUMBER) |
|
108 |
+battemp = tmp102.tmp102(_BAT_TMP_SENSOR_ADDR, _BUS_NUMBER) |
|
109 |
+ambtemp = tmp102.tmp102(_AMB_TMP_SENSOR_ADDR, _BUS_NUMBER) |
|
101 | 110 |
|
102 | 111 |
### PRIVATE METHODS ### |
103 | 112 |
|
... | ... |
@@ -127,21 +136,37 @@ def getEpochSeconds(sTime): |
127 | 136 |
return tSeconds |
128 | 137 |
## end def |
129 | 138 |
|
130 |
-def terminateAgentProcess(signal, frame): |
|
131 |
- """ |
|
132 |
- Send a message to the log when the agent process gets killed |
|
133 |
- by the operating system. Inform downstream clients |
|
134 |
- by removing output data files. |
|
135 |
- Parameters: |
|
136 |
- signal, frame - dummy parameters |
|
137 |
- Returns: nothing |
|
139 |
+def setStatusToOffline(): |
|
140 |
+ """Set the detected status of the device to |
|
141 |
+ "offline" and inform downstream clients by removing input |
|
142 |
+ and output data files. |
|
143 |
+ Parameters: none |
|
144 |
+ Returns: nothing |
|
138 | 145 |
""" |
146 |
+ global deviceOnline |
|
147 |
+ |
|
139 | 148 |
# Inform downstream clients by removing output data file. |
140 | 149 |
if os.path.exists(_OUTPUT_DATA_FILE): |
141 | 150 |
os.remove(_OUTPUT_DATA_FILE) |
142 |
- print('%s terminating npw agent process' % getTimeStamp()) |
|
151 |
+ # If the sensor or device was previously online, then send |
|
152 |
+ # a message that we are now offline. |
|
153 |
+ if deviceOnline: |
|
154 |
+ print('%s device offline' % getTimeStamp()) |
|
155 |
+ deviceOnline = False |
|
156 |
+##end def |
|
157 |
+ |
|
158 |
+def terminateAgentProcess(signal, frame): |
|
159 |
+ """Send a message to log when the agent process gets killed |
|
160 |
+ by the operating system. Inform downstream clients |
|
161 |
+ by removing input and output data files. |
|
162 |
+ Parameters: |
|
163 |
+ signal, frame - dummy parameters |
|
164 |
+ Returns: nothing |
|
165 |
+ """ |
|
166 |
+ print('%s terminating agent process' % getTimeStamp()) |
|
167 |
+ setStatusToOffline() |
|
143 | 168 |
sys.exit(0) |
144 |
-## end def |
|
169 |
+##end def |
|
145 | 170 |
|
146 | 171 |
### PUBLIC METHODS ### |
147 | 172 |
|
... | ... |
@@ -156,11 +181,11 @@ def getSensorData(dData): |
156 | 181 |
dData["time"] = getTimeStamp() |
157 | 182 |
|
158 | 183 |
try: |
159 |
- dData["current"] = pwr.getCurrent() |
|
160 |
- dData["voltage"] = pwr.getVoltage() |
|
161 |
- dData["power"] = pwr.getPower() |
|
162 |
- dData["battemp"] = btmp.getTempF() |
|
163 |
- dData["ambtemp"] = atmp.getTempF() |
|
184 |
+ dData["current"] = power.getCurrent() |
|
185 |
+ dData["voltage"] = power.getVoltage() |
|
186 |
+ dData["power"] = power.getPower() |
|
187 |
+ dData["battemp"] = battemp.getTempF() |
|
188 |
+ dData["ambtemp"] = ambtemp.getTempF() |
|
164 | 189 |
except Exception as exError: |
165 | 190 |
print("%s sensor error: %s" % (getTimeStamp(), exError)) |
166 | 191 |
return False |
... | ... |
@@ -212,6 +237,35 @@ def writeOutputFile(dData): |
212 | 237 |
return True |
213 | 238 |
## end def |
214 | 239 |
|
240 |
+def setStatus(updateSuccess): |
|
241 |
+ """Detect if device is offline or not available on |
|
242 |
+ the network. After a set number of attempts to get data |
|
243 |
+ from the device set a flag that the device is offline. |
|
244 |
+ Parameters: |
|
245 |
+ updateSuccess - a boolean that is True if data request |
|
246 |
+ successful, False otherwise |
|
247 |
+ Returns: nothing |
|
248 |
+ """ |
|
249 |
+ global failedUpdateCount, deviceOnline |
|
250 |
+ |
|
251 |
+ if updateSuccess: |
|
252 |
+ failedUpdateCount = 0 |
|
253 |
+ # Set status and send a message to the log if the device |
|
254 |
+ # previously offline and is now online. |
|
255 |
+ if not deviceOnline: |
|
256 |
+ print('%s device online' % getTimeStamp()) |
|
257 |
+ deviceOnline = True |
|
258 |
+ else: |
|
259 |
+ # The last attempt failed, so update the failed attempts |
|
260 |
+ # count. |
|
261 |
+ failedUpdateCount += 1 |
|
262 |
+ |
|
263 |
+ if failedUpdateCount >= _MAX_FAILED_DATA_REQUESTS: |
|
264 |
+ # Max number of failed data requests, so set |
|
265 |
+ # device status to offline. |
|
266 |
+ setStatusToOffline() |
|
267 |
+##end def |
|
268 |
+ |
|
215 | 269 |
def updateDatabase(dData): |
216 | 270 |
""" |
217 | 271 |
Update the rrdtool database by executing an rrdtool system command. |
... | ... |
@@ -439,12 +493,13 @@ def main(): |
439 | 493 |
Parameters: none |
440 | 494 |
Returns: nothing |
441 | 495 |
""" |
442 |
- global dataRequestInterval |
|
443 |
- |
|
444 | 496 |
signal.signal(signal.SIGTERM, terminateAgentProcess) |
445 | 497 |
signal.signal(signal.SIGINT, terminateAgentProcess) |
446 | 498 |
|
447 |
- print('%s starting up node power agent process' % getTimeStamp()) |
|
499 |
+ # Log agent process startup time. |
|
500 |
+ print '===================\n'\ |
|
501 |
+ '%s starting up node power agent process' % \ |
|
502 |
+ (getTimeStamp()) |
|
448 | 503 |
|
449 | 504 |
# last time output JSON file updated |
450 | 505 |
lastDataRequestTime = -1 |
... | ... |
@@ -488,6 +543,8 @@ def main(): |
488 | 543 |
## Update the round robin database with the parsed data. |
489 | 544 |
result = updateDatabase(dData) |
490 | 545 |
|
546 |
+ setStatus(result) |
|
547 |
+ |
|
491 | 548 |
# At the chart generation interval, generate charts. |
492 | 549 |
if currentTime - lastChartUpdateTime > chartUpdateInterval: |
493 | 550 |
lastChartUpdateTime = currentTime |
... | ... |
@@ -86,15 +86,13 @@ _AVERAGE_LINE_COLOR = '#006600' |
86 | 86 |
### GLOBAL VARIABLES ### |
87 | 87 |
|
88 | 88 |
# debug output options |
89 |
-debugOption = False |
|
90 |
-verboseDebug = False |
|
89 |
+debugMode = False |
|
90 |
+verboseMode = False |
|
91 | 91 |
|
92 | 92 |
# frequency of data requests to sensors |
93 | 93 |
dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL |
94 | 94 |
# how often charts get updated |
95 | 95 |
chartUpdateInterval = _CHART_UPDATE_INTERVAL |
96 |
-# last node request time |
|
97 |
-lastDataPointTime = -1 |
|
98 | 96 |
|
99 | 97 |
# Define each sensor. This also initialzes each sensor. |
100 | 98 |
pwr = ina260.ina260(_PWR_SENSOR_ADDR, _BUS_SEL) |
... | ... |
@@ -170,41 +168,6 @@ def getSensorData(dData): |
170 | 168 |
return True |
171 | 169 |
## end def |
172 | 170 |
|
173 |
-def updateDatabase(dData): |
|
174 |
- """ |
|
175 |
- Update the rrdtool database by executing an rrdtool system command. |
|
176 |
- Format the command using the data extracted from the sensors. |
|
177 |
- Parameters: dData - dictionary object containing data items to be |
|
178 |
- written to the rr database file |
|
179 |
- Returns: True if successful, False otherwise |
|
180 |
- """ |
|
181 |
- |
|
182 |
- epochTime = getEpochSeconds(dData['time']) |
|
183 |
- |
|
184 |
- # Format the rrdtool update command. |
|
185 |
- strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s" |
|
186 |
- strCmd = strFmt % (_RRD_FILE, epochTime, dData['current'], \ |
|
187 |
- dData['voltage'], dData['power'], dData['battemp'], \ |
|
188 |
- dData['ambtemp']) |
|
189 |
- |
|
190 |
- if verboseDebug: |
|
191 |
- print("%s" % strCmd) # DEBUG |
|
192 |
- |
|
193 |
- # Run the command as a subprocess. |
|
194 |
- try: |
|
195 |
- subprocess.check_output(strCmd, shell=True, \ |
|
196 |
- stderr=subprocess.STDOUT) |
|
197 |
- except subprocess.CalledProcessError as exError: |
|
198 |
- print("%s: rrdtool update failed: %s" % \ |
|
199 |
- (getTimeStamp(), exError.output)) |
|
200 |
- return False |
|
201 |
- |
|
202 |
- if debugOption and not verboseDebug: |
|
203 |
- print("database updated") |
|
204 |
- |
|
205 |
- return True |
|
206 |
-## end def |
|
207 |
- |
|
208 | 171 |
def writeOutputFile(dData): |
209 | 172 |
""" |
210 | 173 |
Write sensor data items to the output data file, formatted as |
... | ... |
@@ -222,8 +185,8 @@ def writeOutputFile(dData): |
222 | 185 |
# * The sensor values |
223 | 186 |
|
224 | 187 |
# Create a JSON formatted string from the sensor data. |
188 |
+ jsData = json.loads("{}") |
|
225 | 189 |
try: |
226 |
- jsData = json.loads("{}") |
|
227 | 190 |
for key in dData: |
228 | 191 |
jsData.update({key:dData[key]}) |
229 | 192 |
jsData.update({"chartUpdateInterval": chartUpdateInterval}) |
... | ... |
@@ -232,7 +195,7 @@ def writeOutputFile(dData): |
232 | 195 |
print("%s writeOutputFile: %s" % (getTimeStamp(), exError)) |
233 | 196 |
return False |
234 | 197 |
|
235 |
- if verboseDebug: |
|
198 |
+ if debugMode: |
|
236 | 199 |
print(sData) |
237 | 200 |
|
238 | 201 |
# Write the JSON formatted data to the output data file. |
... | ... |
@@ -249,6 +212,41 @@ def writeOutputFile(dData): |
249 | 212 |
return True |
250 | 213 |
## end def |
251 | 214 |
|
215 |
+def updateDatabase(dData): |
|
216 |
+ """ |
|
217 |
+ Update the rrdtool database by executing an rrdtool system command. |
|
218 |
+ Format the command using the data extracted from the sensors. |
|
219 |
+ Parameters: dData - dictionary object containing data items to be |
|
220 |
+ written to the rr database file |
|
221 |
+ Returns: True if successful, False otherwise |
|
222 |
+ """ |
|
223 |
+ |
|
224 |
+ epochTime = getEpochSeconds(dData['time']) |
|
225 |
+ |
|
226 |
+ # Format the rrdtool update command. |
|
227 |
+ strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s" |
|
228 |
+ strCmd = strFmt % (_RRD_FILE, epochTime, dData['current'], \ |
|
229 |
+ dData['voltage'], dData['power'], dData['battemp'], \ |
|
230 |
+ dData['ambtemp']) |
|
231 |
+ |
|
232 |
+ if debugMode: |
|
233 |
+ print("%s" % strCmd) # DEBUG |
|
234 |
+ |
|
235 |
+ # Run the command as a subprocess. |
|
236 |
+ try: |
|
237 |
+ subprocess.check_output(strCmd, shell=True, \ |
|
238 |
+ stderr=subprocess.STDOUT) |
|
239 |
+ except subprocess.CalledProcessError as exError: |
|
240 |
+ print("%s: rrdtool update failed: %s" % \ |
|
241 |
+ (getTimeStamp(), exError.output)) |
|
242 |
+ return False |
|
243 |
+ |
|
244 |
+ if verboseMode and not debugMode: |
|
245 |
+ print("database updated") |
|
246 |
+ |
|
247 |
+ return True |
|
248 |
+## end def |
|
249 |
+ |
|
252 | 250 |
def createGraph(fileName, dataItem, gLabel, gTitle, gStart, |
253 | 251 |
lower=0, upper=0, trendLine=0, scaleFactor=1, |
254 | 252 |
autoScale=True, alertLine=""): |
... | ... |
@@ -314,8 +312,8 @@ def createGraph(fileName, dataItem, gLabel, gTitle, gStart, |
314 | 312 |
if alertLine != "": |
315 | 313 |
strCmd += "HRULE:%s#FF0000:Critical\ Low\ Voltage " % (alertLine) |
316 | 314 |
|
317 |
- if verboseDebug: |
|
318 |
- print("%s" % strCmd) # DEBUG |
|
315 |
+ if debugMode: |
|
316 |
+ print("%s\n" % strCmd) # DEBUG |
|
319 | 317 |
|
320 | 318 |
# Run the formatted rrdtool command as a subprocess. |
321 | 319 |
try: |
... | ... |
@@ -326,7 +324,7 @@ def createGraph(fileName, dataItem, gLabel, gTitle, gStart, |
326 | 324 |
print("rrdtool graph failed: %s" % (exError.output)) |
327 | 325 |
return False |
328 | 326 |
|
329 |
- if debugOption: |
|
327 |
+ if verboseMode and not debugMode: |
|
330 | 328 |
print("rrdtool graph: %s" % result.decode('utf-8')) |
331 | 329 |
return True |
332 | 330 |
|
... | ... |
@@ -398,20 +396,20 @@ def getCLarguments(): |
398 | 396 |
""" |
399 | 397 |
Get command line arguments. There are three possible arguments |
400 | 398 |
-d turns on debug mode |
401 |
- -v turns on verbose debug mode |
|
399 |
+ -v turns on verbose mode |
|
402 | 400 |
-p sets the sensor query period |
403 | 401 |
-c sets the chart update period |
404 | 402 |
Returns: nothing |
405 | 403 |
""" |
406 |
- global debugOption, verboseDebug, dataRequestInterval, chartUpdateInterval |
|
404 |
+ global debugMode, verboseMode, dataRequestInterval, chartUpdateInterval |
|
407 | 405 |
|
408 | 406 |
index = 1 |
409 | 407 |
while index < len(sys.argv): |
410 |
- if sys.argv[index] == '-d': |
|
411 |
- debugOption = True |
|
412 |
- elif sys.argv[index] == '-v': |
|
413 |
- debugOption = True |
|
414 |
- verboseDebug = True |
|
408 |
+ if sys.argv[index] == '-v': |
|
409 |
+ verboseMode = True |
|
410 |
+ elif sys.argv[index] == '-d': |
|
411 |
+ debugMode = True |
|
412 |
+ verboseMode = True |
|
415 | 413 |
elif sys.argv[index] == '-p': |
416 | 414 |
try: |
417 | 415 |
dataRequestInterval = abs(int(sys.argv[index + 1])) |
... | ... |
@@ -475,7 +473,6 @@ def main(): |
475 | 473 |
if currentTime - lastDataRequestTime > dataRequestInterval: |
476 | 474 |
lastDataRequestTime = currentTime |
477 | 475 |
dData = {} |
478 |
- result = True |
|
479 | 476 |
|
480 | 477 |
# Get the data from the sensors. |
481 | 478 |
result =getSensorData(dData) |
... | ... |
@@ -501,7 +498,7 @@ def main(): |
501 | 498 |
# the next update interval. |
502 | 499 |
|
503 | 500 |
elapsedTime = time.time() - currentTime |
504 |
- if debugOption: |
|
501 |
+ if verboseMode: |
|
505 | 502 |
if result: |
506 | 503 |
print("update successful: %6f sec\n" |
507 | 504 |
% elapsedTime) |
... | ... |
@@ -39,11 +39,15 @@ import signal |
39 | 39 |
import subprocess |
40 | 40 |
import multiprocessing |
41 | 41 |
import time |
42 |
+import json |
|
42 | 43 |
|
43 | 44 |
# Import sensor libraries. |
44 | 45 |
import ina260 # power sensor |
45 | 46 |
import tmp102 # temperature sensor |
46 | 47 |
|
48 |
+ ### ENVIRONMENT ### |
|
49 |
+_USER = os.environ['USER'] |
|
50 |
+ |
|
47 | 51 |
### SENSOR BUS ADDRESSES ### |
48 | 52 |
|
49 | 53 |
# Set bus addresses of sensors. |
... | ... |
@@ -55,8 +59,7 @@ _BUS_SEL = 1 |
55 | 59 |
|
56 | 60 |
### FILE AND FOLDER LOCATIONS ### |
57 | 61 |
|
58 |
-_USER = os.environ['USER'] |
|
59 |
-# folder to contain dynamic data objects |
|
62 |
+# folder to contain html |
|
60 | 63 |
_DOCROOT_PATH = "/home/%s/public_html/power/" % _USER |
61 | 64 |
# folder to contain charts and output data file |
62 | 65 |
_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/" |
... | ... |
@@ -67,10 +70,10 @@ _RRD_FILE = "/home/%s/database/powerData.rrd" % _USER |
67 | 70 |
|
68 | 71 |
### GLOBAL CONSTANTS ### |
69 | 72 |
|
70 |
-# rrdtool database update interval in seconds |
|
71 |
-_DATABASE_UPDATE_INTERVAL = 30 |
|
72 | 73 |
# sensor data request interval in seconds |
73 | 74 |
_DEFAULT_DATA_REQUEST_INTERVAL = 2 |
75 |
+# rrdtool database update interval in seconds |
|
76 |
+_DATABASE_UPDATE_INTERVAL = 30 |
|
74 | 77 |
# chart update interval in seconds |
75 | 78 |
_CHART_UPDATE_INTERVAL = 600 |
76 | 79 |
# standard chart width in pixels |
... | ... |
@@ -85,6 +88,7 @@ _AVERAGE_LINE_COLOR = '#006600' |
85 | 88 |
# debug output options |
86 | 89 |
debugOption = False |
87 | 90 |
verboseDebug = False |
91 |
+ |
|
88 | 92 |
# frequency of data requests to sensors |
89 | 93 |
dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL |
90 | 94 |
# how often charts get updated |
... | ... |
@@ -101,7 +105,7 @@ atmp = tmp102.tmp102(_AMB_TMP_SENSOR_ADDR, _BUS_SEL) |
101 | 105 |
|
102 | 106 |
def getTimeStamp(): |
103 | 107 |
""" |
104 |
- Set the error message time stamp to the local system time. |
|
108 |
+ Get the local time and format as a text string. |
|
105 | 109 |
Parameters: none |
106 | 110 |
Returns: string containing the time stamp |
107 | 111 |
""" |
... | ... |
@@ -109,57 +113,58 @@ def getTimeStamp(): |
109 | 113 |
## end def |
110 | 114 |
|
111 | 115 |
def getEpochSeconds(sTime): |
112 |
- """Convert the time stamp supplied in the weather data string |
|
113 |
- to seconds since 1/1/1970 00:00:00. |
|
114 |
- Parameters: |
|
115 |
- sTime - the time stamp to be converted must be formatted |
|
116 |
+ """ |
|
117 |
+ Convert the time stamp to seconds since 1/1/1970 00:00:00. |
|
118 |
+ Parameters: |
|
119 |
+ sTime - the time stamp to be converted must be formatted |
|
116 | 120 |
as %m/%d/%Y %H:%M:%S |
117 |
- Returns: epoch seconds |
|
121 |
+ Returns: epoch seconds |
|
118 | 122 |
""" |
119 | 123 |
try: |
120 | 124 |
t_sTime = time.strptime(sTime, '%m/%d/%Y %H:%M:%S') |
121 |
- except Exception, exError: |
|
122 |
- print '%s getEpochSeconds: %s' % (getTimeStamp(), exError) |
|
125 |
+ except Exception as exError: |
|
126 |
+ print('%s getEpochSeconds: %s' % (getTimeStamp(), exError)) |
|
123 | 127 |
return None |
124 | 128 |
tSeconds = int(time.mktime(t_sTime)) |
125 | 129 |
return tSeconds |
126 | 130 |
## end def |
127 | 131 |
|
128 | 132 |
def terminateAgentProcess(signal, frame): |
129 |
- """Send a message to the log when the agent process gets killed |
|
130 |
- by the operating system. Inform downstream clients |
|
131 |
- by removing output data files. |
|
132 |
- Parameters: |
|
133 |
- signal, frame - dummy parameters |
|
134 |
- Returns: nothing |
|
133 |
+ """ |
|
134 |
+ Send a message to the log when the agent process gets killed |
|
135 |
+ by the operating system. Inform downstream clients |
|
136 |
+ by removing output data files. |
|
137 |
+ Parameters: |
|
138 |
+ signal, frame - dummy parameters |
|
139 |
+ Returns: nothing |
|
135 | 140 |
""" |
136 | 141 |
# Inform downstream clients by removing output data file. |
137 | 142 |
if os.path.exists(_OUTPUT_DATA_FILE): |
138 | 143 |
os.remove(_OUTPUT_DATA_FILE) |
139 |
- print '%s terminating npw agent process' % \ |
|
140 |
- (getTimeStamp()) |
|
144 |
+ print('%s terminating npw agent process' % getTimeStamp()) |
|
141 | 145 |
sys.exit(0) |
142 | 146 |
## end def |
143 | 147 |
|
144 | 148 |
### PUBLIC METHODS ### |
145 | 149 |
|
146 | 150 |
def getSensorData(dData): |
147 |
- """Poll sensors for data. Store the data in a dictionary object for |
|
148 |
- use by other subroutines. The dictionary object passed in should |
|
149 |
- an empty dictionary, i.e., dData = { }. |
|
150 |
- Parameters: dData - a dictionary object to contain the sensor data |
|
151 |
- Returns: True if successful, False otherwise |
|
152 | 151 |
""" |
152 |
+ Poll sensors for data. Store the data in a dictionary object for |
|
153 |
+ use by other subroutines. The dictionary object passed in should |
|
154 |
+ an empty dictionary, i.e., dData = { }. |
|
155 |
+ Parameters: dData - a dictionary object to contain the sensor data |
|
156 |
+ Returns: True if successful, False otherwise |
|
157 |
+ """ |
|
158 |
+ dData["time"] = getTimeStamp() |
|
159 |
+ |
|
153 | 160 |
try: |
154 |
- dData["time"] = getTimeStamp() |
|
155 | 161 |
dData["current"] = pwr.getCurrent() |
156 | 162 |
dData["voltage"] = pwr.getVoltage() |
157 | 163 |
dData["power"] = pwr.getPower() |
158 | 164 |
dData["battemp"] = btmp.getTempF() |
159 | 165 |
dData["ambtemp"] = atmp.getTempF() |
160 |
- |
|
161 |
- except Exception, exError: |
|
162 |
- print "%s sensor error: %s" % (getTimeStamp(), exError) |
|
166 |
+ except Exception as exError: |
|
167 |
+ print("%s sensor error: %s" % (getTimeStamp(), exError)) |
|
163 | 168 |
return False |
164 | 169 |
|
165 | 170 |
return True |
... | ... |
@@ -182,29 +187,33 @@ def updateDatabase(dData): |
182 | 187 |
dData['voltage'], dData['power'], dData['battemp'], \ |
183 | 188 |
dData['ambtemp']) |
184 | 189 |
|
185 |
- if debugOption: |
|
186 |
- print "%s" % strCmd # DEBUG |
|
190 |
+ if verboseDebug: |
|
191 |
+ print("%s" % strCmd) # DEBUG |
|
187 | 192 |
|
188 | 193 |
# Run the command as a subprocess. |
189 | 194 |
try: |
190 |
- subprocess.check_output(strCmd, shell=True, \ |
|
191 |
- stderr=subprocess.STDOUT) |
|
192 |
- except subprocess.CalledProcessError, exError: |
|
193 |
- print "%s: rrdtool update failed: %s" % \ |
|
194 |
- (getTimeStamp(), exError.output) |
|
195 |
+ subprocess.check_output(strCmd, shell=True, \ |
|
196 |
+ stderr=subprocess.STDOUT) |
|
197 |
+ except subprocess.CalledProcessError as exError: |
|
198 |
+ print("%s: rrdtool update failed: %s" % \ |
|
199 |
+ (getTimeStamp(), exError.output)) |
|
195 | 200 |
return False |
196 | 201 |
|
202 |
+ if debugOption and not verboseDebug: |
|
203 |
+ print("database updated") |
|
204 |
+ |
|
197 | 205 |
return True |
198 | 206 |
## end def |
199 | 207 |
|
200 |
-def writeOutputDataFile(dData): |
|
201 |
- """Write node data items to the output data file, formatted as |
|
202 |
- a Javascript file. This file may then be requested and used by |
|
203 |
- by downstream clients, for instance, an HTML document. |
|
204 |
- Parameters: |
|
205 |
- dData - a dictionary containing the data to be written |
|
208 |
+def writeOutputFile(dData): |
|
209 |
+ """ |
|
210 |
+ Write sensor data items to the output data file, formatted as |
|
211 |
+ a Javascript file. This file may then be requested and used by |
|
212 |
+ by downstream clients, for instance, an HTML document. |
|
213 |
+ Parameters: |
|
214 |
+ dData - a dictionary containing the data to be written |
|
206 | 215 |
to the output data file |
207 |
- Returns: True if successful, False otherwise |
|
216 |
+ Returns: True if successful, False otherwise |
|
208 | 217 |
""" |
209 | 218 |
# Write a JSON formatted file for use by html clients. The following |
210 | 219 |
# data items are sent to the client file. |
... | ... |
@@ -213,54 +222,58 @@ def writeOutputDataFile(dData): |
213 | 222 |
# * The sensor values |
214 | 223 |
|
215 | 224 |
# Create a JSON formatted string from the sensor data. |
216 |
- sData = "[{\"period\":\"%s\", " % \ |
|
217 |
- (chartUpdateInterval) |
|
218 |
- for key in dData: |
|
219 |
- sData += '\"%s\":\"%s\", ' % (key, dData[key]) |
|
220 |
- sData = sData[:-2] + '}]\n' |
|
225 |
+ try: |
|
226 |
+ jsData = json.loads("{}") |
|
227 |
+ for key in dData: |
|
228 |
+ jsData.update({key:dData[key]}) |
|
229 |
+ jsData.update({"chartUpdateInterval": chartUpdateInterval}) |
|
230 |
+ sData = "[%s]" % json.dumps(jsData) |
|
231 |
+ except Exception as exError: |
|
232 |
+ print("%s writeOutputFile: %s" % (getTimeStamp(), exError)) |
|
233 |
+ return False |
|
234 |
+ |
|
235 |
+ if verboseDebug: |
|
236 |
+ print(sData) |
|
221 | 237 |
|
222 | 238 |
# Write the JSON formatted data to the output data file. |
239 |
+ |
|
223 | 240 |
try: |
224 | 241 |
fc = open(_OUTPUT_DATA_FILE, "w") |
225 | 242 |
fc.write(sData) |
226 | 243 |
fc.close() |
227 |
- except Exception, exError: |
|
228 |
- print "%s write output file failed: %s" % \ |
|
229 |
- (getTimeStamp(), exError) |
|
244 |
+ except Exception as exError: |
|
245 |
+ print("%s write output file failed: %s" % \ |
|
246 |
+ (getTimeStamp(), exError)) |
|
230 | 247 |
return False |
231 | 248 |
|
232 |
- if verboseDebug: |
|
233 |
- print sData[:-1] |
|
234 |
- if debugOption: |
|
235 |
- print "writing output data file: %d bytes" % len(sData) |
|
236 |
- |
|
237 | 249 |
return True |
238 | 250 |
## end def |
239 | 251 |
|
240 | 252 |
def createGraph(fileName, dataItem, gLabel, gTitle, gStart, |
241 | 253 |
lower=0, upper=0, trendLine=0, scaleFactor=1, |
242 | 254 |
autoScale=True, alertLine=""): |
243 |
- """Uses rrdtool to create a graph of specified node data item. |
|
244 |
- Parameters: |
|
245 |
- fileName - name of file containing the graph |
|
246 |
- dataItem - data item to be graphed |
|
247 |
- gLabel - string containing a graph label for the data item |
|
248 |
- gTitle - string containing a title for the graph |
|
249 |
- gStart - beginning time of the graphed data |
|
250 |
- lower - lower bound for graph ordinate #NOT USED |
|
251 |
- upper - upper bound for graph ordinate #NOT USED |
|
252 |
- trendLine |
|
253 |
- 0, show only graph data |
|
254 |
- 1, show only a trend line |
|
255 |
- 2, show a trend line and the graph data |
|
256 |
- scaleFactor - amount to pre-scale the data before charting |
|
257 |
- the data [default=1] |
|
258 |
- autoScale - if True, then use vertical axis auto scaling |
|
259 |
- (lower and upper parameters must be zero) |
|
260 |
- alertLine - value for which to print a critical |
|
261 |
- low voltage alert line on the chart. If not provided |
|
262 |
- alert line will not be printed. |
|
263 |
- Returns: True if successful, False otherwise |
|
255 |
+ """ |
|
256 |
+ Uses rrdtool to create a graph of specified sensor data item. |
|
257 |
+ Parameters: |
|
258 |
+ fileName - name of file containing the graph |
|
259 |
+ dataItem - data item to be graphed |
|
260 |
+ gLabel - string containing a graph label for the data item |
|
261 |
+ gTitle - string containing a title for the graph |
|
262 |
+ gStart - beginning time of the graphed data |
|
263 |
+ lower - lower bound for graph ordinate #NOT USED |
|
264 |
+ upper - upper bound for graph ordinate #NOT USED |
|
265 |
+ trendLine |
|
266 |
+ 0, show only graph data |
|
267 |
+ 1, show only a trend line |
|
268 |
+ 2, show a trend line and the graph data |
|
269 |
+ scaleFactor - amount to pre-scale the data before charting |
|
270 |
+ the data [default=1] |
|
271 |
+ autoScale - if True, then use vertical axis auto scaling |
|
272 |
+ (lower and upper parameters must be zero) |
|
273 |
+ alertLine - value for which to print a critical |
|
274 |
+ low voltage alert line on the chart. If not provided |
|
275 |
+ alert line will not be printed. |
|
276 |
+ Returns: True if successful, False otherwise |
|
264 | 277 |
""" |
265 | 278 |
gPath = _CHARTS_DIRECTORY + fileName + ".png" |
266 | 279 |
trendWindow = { 'end-1day': 7200, |
... | ... |
@@ -302,27 +315,28 @@ def createGraph(fileName, dataItem, gLabel, gTitle, gStart, |
302 | 315 |
strCmd += "HRULE:%s#FF0000:Critical\ Low\ Voltage " % (alertLine) |
303 | 316 |
|
304 | 317 |
if verboseDebug: |
305 |
- print "%s" % strCmd # DEBUG |
|
318 |
+ print("%s" % strCmd) # DEBUG |
|
306 | 319 |
|
307 | 320 |
# Run the formatted rrdtool command as a subprocess. |
308 | 321 |
try: |
309 | 322 |
result = subprocess.check_output(strCmd, \ |
310 | 323 |
stderr=subprocess.STDOUT, \ |
311 | 324 |
shell=True) |
312 |
- except subprocess.CalledProcessError, exError: |
|
313 |
- print "rrdtool graph failed: %s" % (exError.output) |
|
325 |
+ except subprocess.CalledProcessError as exError: |
|
326 |
+ print("rrdtool graph failed: %s" % (exError.output)) |
|
314 | 327 |
return False |
315 | 328 |
|
316 | 329 |
if debugOption: |
317 |
- print "rrdtool graph: %s\n" % result, |
|
330 |
+ print("rrdtool graph: %s" % result.decode('utf-8')) |
|
318 | 331 |
return True |
319 | 332 |
|
320 | 333 |
## end def |
321 | 334 |
|
322 | 335 |
def generateGraphs(): |
323 |
- """Generate graphs for display in html documents. |
|
324 |
- Parameters: none |
|
325 |
- Returns: nothing |
|
336 |
+ """ |
|
337 |
+ Generate graphs for display in html documents. |
|
338 |
+ Parameters: none |
|
339 |
+ Returns: nothing |
|
326 | 340 |
""" |
327 | 341 |
|
328 | 342 |
# 24 hour stock charts |
... | ... |
@@ -381,12 +395,13 @@ def generateGraphs(): |
381 | 395 |
## end def |
382 | 396 |
|
383 | 397 |
def getCLarguments(): |
384 |
- """Get command line arguments. There are three possible arguments |
|
385 |
- -d turns on debug mode |
|
386 |
- -v turns on verbose debug mode |
|
387 |
- -p sets the sensor query period |
|
388 |
- -c sets the chart update period |
|
389 |
- Returns: nothing |
|
398 |
+ """ |
|
399 |
+ Get command line arguments. There are three possible arguments |
|
400 |
+ -d turns on debug mode |
|
401 |
+ -v turns on verbose debug mode |
|
402 |
+ -p sets the sensor query period |
|
403 |
+ -c sets the chart update period |
|
404 |
+ Returns: nothing |
|
390 | 405 |
""" |
391 | 406 |
global debugOption, verboseDebug, dataRequestInterval, chartUpdateInterval |
392 | 407 |
|
... | ... |
@@ -401,36 +416,37 @@ def getCLarguments(): |
401 | 416 |
try: |
402 | 417 |
dataRequestInterval = abs(int(sys.argv[index + 1])) |
403 | 418 |
except: |
404 |
- print "invalid sensor query period" |
|
419 |
+ print("invalid sensor query period") |
|
405 | 420 |
exit(-1) |
406 | 421 |
index += 1 |
407 | 422 |
elif sys.argv[index] == '-c': |
408 | 423 |
try: |
409 | 424 |
chartUpdateInterval = abs(int(sys.argv[index + 1])) |
410 | 425 |
except: |
411 |
- print "invalid chart update period" |
|
426 |
+ print("invalid chart update period") |
|
412 | 427 |
exit(-1) |
413 | 428 |
index += 1 |
414 | 429 |
else: |
415 | 430 |
cmd_name = sys.argv[0].split('/') |
416 |
- print "Usage: %s [-d | v] [-p seconds] [-c seconds]" \ |
|
417 |
- % cmd_name[-1] |
|
431 |
+ print("Usage: %s [-d | v] [-p seconds] [-c seconds]" \ |
|
432 |
+ % cmd_name[-1]) |
|
418 | 433 |
exit(-1) |
419 | 434 |
index += 1 |
420 | 435 |
##end def |
421 | 436 |
|
422 | 437 |
def main(): |
423 |
- """Handles timing of events and acts as executive routine managing |
|
424 |
- all other functions. |
|
425 |
- Parameters: none |
|
426 |
- Returns: nothing |
|
438 |
+ """ |
|
439 |
+ Handles timing of events and acts as executive routine managing |
|
440 |
+ all other functions. |
|
441 |
+ Parameters: none |
|
442 |
+ Returns: nothing |
|
427 | 443 |
""" |
428 | 444 |
global dataRequestInterval |
429 | 445 |
|
430 | 446 |
signal.signal(signal.SIGTERM, terminateAgentProcess) |
447 |
+ signal.signal(signal.SIGINT, terminateAgentProcess) |
|
431 | 448 |
|
432 |
- print '%s starting up node power agent process' % \ |
|
433 |
- (getTimeStamp()) |
|
449 |
+ print('%s starting up node power agent process' % getTimeStamp()) |
|
434 | 450 |
|
435 | 451 |
# last time output JSON file updated |
436 | 452 |
lastDataRequestTime = -1 |
... | ... |
@@ -444,9 +460,9 @@ def main(): |
444 | 460 |
|
445 | 461 |
## Exit with error if rrdtool database does not exist. |
446 | 462 |
if not os.path.exists(_RRD_FILE): |
447 |
- print 'rrdtool database does not exist\n' \ |
|
448 |
- 'use createArednsigRrd script to ' \ |
|
449 |
- 'create rrdtool database\n' |
|
463 |
+ print('rrdtool database does not exist\n' \ |
|
464 |
+ 'use createPowerRrd script to ' \ |
|
465 |
+ 'create rrdtool database\n') |
|
450 | 466 |
exit(1) |
451 | 467 |
|
452 | 468 |
## main loop |
... | ... |
@@ -454,8 +470,8 @@ def main(): |
454 | 470 |
|
455 | 471 |
currentTime = time.time() # get current time in seconds |
456 | 472 |
|
457 |
- # Every web update interval request data from the aredn |
|
458 |
- # node and process the received data. |
|
473 |
+ # Every data request interval read the sensors and process the |
|
474 |
+ # data from the sensors. |
|
459 | 475 |
if currentTime - lastDataRequestTime > dataRequestInterval: |
460 | 476 |
lastDataRequestTime = currentTime |
461 | 477 |
dData = {} |
... | ... |
@@ -466,17 +482,14 @@ def main(): |
466 | 482 |
|
467 | 483 |
# If get data successful, write data to data files. |
468 | 484 |
if result: |
469 |
- result = writeOutputDataFile(dData) |
|
470 |
- pass |
|
485 |
+ result = writeOutputFile(dData) |
|
471 | 486 |
|
472 | 487 |
# At the rrdtool database update interval, update the database. |
473 |
- if currentTime - lastDatabaseUpdateTime > \ |
|
474 |
- _DATABASE_UPDATE_INTERVAL: |
|
488 |
+ if result and (currentTime - lastDatabaseUpdateTime > \ |
|
489 |
+ _DATABASE_UPDATE_INTERVAL): |
|
475 | 490 |
lastDatabaseUpdateTime = currentTime |
476 | 491 |
## Update the round robin database with the parsed data. |
477 |
- if result: |
|
478 |
- updateDatabase(dData) |
|
479 |
- pass |
|
492 |
+ result = updateDatabase(dData) |
|
480 | 493 |
|
481 | 494 |
# At the chart generation interval, generate charts. |
482 | 495 |
if currentTime - lastChartUpdateTime > chartUpdateInterval: |
... | ... |
@@ -490,10 +503,11 @@ def main(): |
490 | 503 |
elapsedTime = time.time() - currentTime |
491 | 504 |
if debugOption: |
492 | 505 |
if result: |
493 |
- print "%s update successful:" % getTimeStamp(), |
|
506 |
+ print("update successful: %6f sec\n" |
|
507 |
+ % elapsedTime) |
|
494 | 508 |
else: |
495 |
- print "%s update failed:" % getTimeStamp(), |
|
496 |
- print "%6f seconds processing time\n" % elapsedTime |
|
509 |
+ print("update failed: %6f sec\n" |
|
510 |
+ % elapsedTime) |
|
497 | 511 |
remainingTime = dataRequestInterval - elapsedTime |
498 | 512 |
if remainingTime > 0.0: |
499 | 513 |
time.sleep(remainingTime) |
... | ... |
@@ -502,8 +516,4 @@ def main(): |
502 | 516 |
## end def |
503 | 517 |
|
504 | 518 |
if __name__ == '__main__': |
505 |
- try: |
|
506 |
- main() |
|
507 |
- except KeyboardInterrupt: |
|
508 |
- print '\n', |
|
509 |
- terminateAgentProcess('KeyboardInterrupt','Module') |
|
519 |
+ main() |
... | ... |
@@ -46,21 +46,21 @@ import tmp102 # temperature sensor |
46 | 46 |
|
47 | 47 |
### SENSOR BUS ADDRESSES ### |
48 | 48 |
|
49 |
-# Set bus addresses of sensors |
|
49 |
+# Set bus addresses of sensors. |
|
50 | 50 |
_PWR_SENSOR_ADDR = 0X40 |
51 | 51 |
_BAT_TMP_SENSOR_ADDR = 0x48 |
52 | 52 |
_AMB_TMP_SENSOR_ADDR = 0x4B |
53 |
-# Set bus selector |
|
53 |
+# Set bus selector. |
|
54 | 54 |
_BUS_SEL = 1 |
55 | 55 |
|
56 | 56 |
### FILE AND FOLDER LOCATIONS ### |
57 | 57 |
|
58 | 58 |
_USER = os.environ['USER'] |
59 |
-# folder for containing dynamic data objects |
|
59 |
+# folder to contain dynamic data objects |
|
60 | 60 |
_DOCROOT_PATH = "/home/%s/public_html/power/" % _USER |
61 |
-# folder for charts and output data file |
|
61 |
+# folder to contain charts and output data file |
|
62 | 62 |
_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/" |
63 |
-# location of data output file |
|
63 |
+# location of JSON output data file |
|
64 | 64 |
_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/powerData.js" |
65 | 65 |
# database that stores node data |
66 | 66 |
_RRD_FILE = "/home/%s/database/powerData.rrd" % _USER |
... | ... |
@@ -82,13 +82,12 @@ _AVERAGE_LINE_COLOR = '#006600' |
82 | 82 |
|
83 | 83 |
### GLOBAL VARIABLES ### |
84 | 84 |
|
85 |
-# turn on or off of verbose debugging information |
|
85 |
+# debug output options |
|
86 | 86 |
debugOption = False |
87 | 87 |
verboseDebug = False |
88 |
- |
|
89 | 88 |
# frequency of data requests to sensors |
90 | 89 |
dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL |
91 |
-# chart update interval |
|
90 |
+# how often charts get updated |
|
92 | 91 |
chartUpdateInterval = _CHART_UPDATE_INTERVAL |
93 | 92 |
# last node request time |
94 | 93 |
lastDataPointTime = -1 |
... | ... |
@@ -127,9 +126,9 @@ def getEpochSeconds(sTime): |
127 | 126 |
## end def |
128 | 127 |
|
129 | 128 |
def terminateAgentProcess(signal, frame): |
130 |
- """Send a message to log when the agent process gets killed |
|
129 |
+ """Send a message to the log when the agent process gets killed |
|
131 | 130 |
by the operating system. Inform downstream clients |
132 |
- by removing input and output data files. |
|
131 |
+ by removing output data files. |
|
133 | 132 |
Parameters: |
134 | 133 |
signal, frame - dummy parameters |
135 | 134 |
Returns: nothing |
... | ... |
@@ -145,10 +144,11 @@ def terminateAgentProcess(signal, frame): |
145 | 144 |
### PUBLIC METHODS ### |
146 | 145 |
|
147 | 146 |
def getSensorData(dData): |
148 |
- """Poll sensors for data. |
|
149 |
- Parameters: none |
|
150 |
- Returns: a string containing the node signal data if successful, |
|
151 |
- or None if not successful |
|
147 |
+ """Poll sensors for data. Store the data in a dictionary object for |
|
148 |
+ use by other subroutines. The dictionary object passed in should |
|
149 |
+ an empty dictionary, i.e., dData = { }. |
|
150 |
+ Parameters: dData - a dictionary object to contain the sensor data |
|
151 |
+ Returns: True if successful, False otherwise |
|
152 | 152 |
""" |
153 | 153 |
try: |
154 | 154 |
dData["time"] = getTimeStamp() |
... | ... |
@@ -199,10 +199,10 @@ def updateDatabase(dData): |
199 | 199 |
|
200 | 200 |
def writeOutputDataFile(dData): |
201 | 201 |
"""Write node data items to the output data file, formatted as |
202 |
- a Javascript file. This file may then be accessed and used by |
|
203 |
- by downstream clients, for instance, in HTML documents. |
|
202 |
+ a Javascript file. This file may then be requested and used by |
|
203 |
+ by downstream clients, for instance, an HTML document. |
|
204 | 204 |
Parameters: |
205 |
- sData - a string object containing the data to be written |
|
205 |
+ dData - a dictionary containing the data to be written |
|
206 | 206 |
to the output data file |
207 | 207 |
Returns: True if successful, False otherwise |
208 | 208 |
""" |
... | ... |
@@ -238,8 +238,8 @@ def writeOutputDataFile(dData): |
238 | 238 |
## end def |
239 | 239 |
|
240 | 240 |
def createGraph(fileName, dataItem, gLabel, gTitle, gStart, |
241 |
- lower, upper, trendLine, scaleFactor=1, autoScale=True, |
|
242 |
- alertLine=""): |
|
241 |
+ lower=0, upper=0, trendLine=0, scaleFactor=1, |
|
242 |
+ autoScale=True, alertLine=""): |
|
243 | 243 |
"""Uses rrdtool to create a graph of specified node data item. |
244 | 244 |
Parameters: |
245 | 245 |
fileName - name of file containing the graph |
... | ... |
@@ -384,10 +384,11 @@ def getCLarguments(): |
384 | 384 |
"""Get command line arguments. There are three possible arguments |
385 | 385 |
-d turns on debug mode |
386 | 386 |
-v turns on verbose debug mode |
387 |
- -t sets the sensor query interval |
|
387 |
+ -p sets the sensor query period |
|
388 |
+ -c sets the chart update period |
|
388 | 389 |
Returns: nothing |
389 | 390 |
""" |
390 |
- global debugOption, verboseDebug, dataRequestInterval |
|
391 |
+ global debugOption, verboseDebug, dataRequestInterval, chartUpdateInterval |
|
391 | 392 |
|
392 | 393 |
index = 1 |
393 | 394 |
while index < len(sys.argv): |
... | ... |
@@ -400,12 +401,20 @@ def getCLarguments(): |
400 | 401 |
try: |
401 | 402 |
dataRequestInterval = abs(int(sys.argv[index + 1])) |
402 | 403 |
except: |
403 |
- print "invalid polling period" |
|
404 |
+ print "invalid sensor query period" |
|
405 |
+ exit(-1) |
|
406 |
+ index += 1 |
|
407 |
+ elif sys.argv[index] == '-c': |
|
408 |
+ try: |
|
409 |
+ chartUpdateInterval = abs(int(sys.argv[index + 1])) |
|
410 |
+ except: |
|
411 |
+ print "invalid chart update period" |
|
404 | 412 |
exit(-1) |
405 | 413 |
index += 1 |
406 | 414 |
else: |
407 | 415 |
cmd_name = sys.argv[0].split('/') |
408 |
- print "Usage: %s [-d] [-v] [-p seconds]" % cmd_name[-1] |
|
416 |
+ print "Usage: %s [-d | v] [-p seconds] [-c seconds]" \ |
|
417 |
+ % cmd_name[-1] |
|
409 | 418 |
exit(-1) |
410 | 419 |
index += 1 |
411 | 420 |
##end def |
1 | 1 |
old mode 100644 |
2 | 2 |
new mode 100755 |
... | ... |
@@ -77,6 +77,8 @@ _CHART_UPDATE_INTERVAL = 600 |
77 | 77 |
_CHART_WIDTH = 600 |
78 | 78 |
# standard chart height in pixels |
79 | 79 |
_CHART_HEIGHT = 150 |
80 |
+# chart average line color |
|
81 |
+_AVERAGE_LINE_COLOR = '#006600' |
|
80 | 82 |
|
81 | 83 |
### GLOBAL VARIABLES ### |
82 | 84 |
|
... | ... |
@@ -105,7 +107,7 @@ def getTimeStamp(): |
105 | 107 |
Returns: string containing the time stamp |
106 | 108 |
""" |
107 | 109 |
return time.strftime( "%m/%d/%Y %T", time.localtime() ) |
108 |
-##end def |
|
110 |
+## end def |
|
109 | 111 |
|
110 | 112 |
def getEpochSeconds(sTime): |
111 | 113 |
"""Convert the time stamp supplied in the weather data string |
... | ... |
@@ -122,7 +124,7 @@ def getEpochSeconds(sTime): |
122 | 124 |
return None |
123 | 125 |
tSeconds = int(time.mktime(t_sTime)) |
124 | 126 |
return tSeconds |
125 |
-##end def |
|
127 |
+## end def |
|
126 | 128 |
|
127 | 129 |
def terminateAgentProcess(signal, frame): |
128 | 130 |
"""Send a message to log when the agent process gets killed |
... | ... |
@@ -138,7 +140,7 @@ def terminateAgentProcess(signal, frame): |
138 | 140 |
print '%s terminating npw agent process' % \ |
139 | 141 |
(getTimeStamp()) |
140 | 142 |
sys.exit(0) |
141 |
-##end def |
|
143 |
+## end def |
|
142 | 144 |
|
143 | 145 |
### PUBLIC METHODS ### |
144 | 146 |
|
... | ... |
@@ -161,7 +163,7 @@ def getSensorData(dData): |
161 | 163 |
return False |
162 | 164 |
|
163 | 165 |
return True |
164 |
-##end def |
|
166 |
+## end def |
|
165 | 167 |
|
166 | 168 |
def updateDatabase(dData): |
167 | 169 |
""" |
... | ... |
@@ -193,7 +195,7 @@ def updateDatabase(dData): |
193 | 195 |
return False |
194 | 196 |
|
195 | 197 |
return True |
196 |
-##end def |
|
198 |
+## end def |
|
197 | 199 |
|
198 | 200 |
def writeOutputDataFile(dData): |
199 | 201 |
"""Write node data items to the output data file, formatted as |
... | ... |
@@ -236,7 +238,8 @@ def writeOutputDataFile(dData): |
236 | 238 |
## end def |
237 | 239 |
|
238 | 240 |
def createGraph(fileName, dataItem, gLabel, gTitle, gStart, |
239 |
- lower, upper, addTrend, autoScale): |
|
241 |
+ lower, upper, trendLine, scaleFactor=1, autoScale=True, |
|
242 |
+ alertLine=""): |
|
240 | 243 |
"""Uses rrdtool to create a graph of specified node data item. |
241 | 244 |
Parameters: |
242 | 245 |
fileName - name of file containing the graph |
... | ... |
@@ -246,12 +249,17 @@ def createGraph(fileName, dataItem, gLabel, gTitle, gStart, |
246 | 249 |
gStart - beginning time of the graphed data |
247 | 250 |
lower - lower bound for graph ordinate #NOT USED |
248 | 251 |
upper - upper bound for graph ordinate #NOT USED |
249 |
- addTrend - 0, show only graph data |
|
250 |
- 1, show only a trend line |
|
251 |
- 2, show a trend line and the graph data |
|
252 |
+ trendLine |
|
253 |
+ 0, show only graph data |
|
254 |
+ 1, show only a trend line |
|
255 |
+ 2, show a trend line and the graph data |
|
256 |
+ scaleFactor - amount to pre-scale the data before charting |
|
257 |
+ the data [default=1] |
|
252 | 258 |
autoScale - if True, then use vertical axis auto scaling |
253 |
- (lower and upper parameters are ignored), otherwise use |
|
254 |
- lower and upper parameters to set vertical axis scale |
|
259 |
+ (lower and upper parameters must be zero) |
|
260 |
+ alertLine - value for which to print a critical |
|
261 |
+ low voltage alert line on the chart. If not provided |
|
262 |
+ alert line will not be printed. |
|
255 | 263 |
Returns: True if successful, False otherwise |
256 | 264 |
""" |
257 | 265 |
gPath = _CHARTS_DIRECTORY + fileName + ".png" |
... | ... |
@@ -277,16 +285,21 @@ def createGraph(fileName, dataItem, gLabel, gTitle, gStart, |
277 | 285 |
|
278 | 286 |
# Show the data, or a moving average trend line over |
279 | 287 |
# the data, or both. |
280 |
- strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem) |
|
281 |
- if addTrend == 0: |
|
288 |
+ strCmd += "DEF:rSeries=%s:%s:LAST " % (_RRD_FILE, dataItem) |
|
289 |
+ strCmd += "CDEF:dSeries=rSeries,%s,/ " % (scaleFactor) |
|
290 |
+ |
|
291 |
+ if trendLine == 0: |
|
282 | 292 |
strCmd += "LINE1:dSeries#0400ff " |
283 |
- elif addTrend == 1: |
|
284 |
- strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE2:smoothed#000000 " \ |
|
285 |
- % trendWindow[gStart] |
|
286 |
- elif addTrend == 2: |
|
293 |
+ elif trendLine == 1: |
|
294 |
+ strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE2:smoothed%s " \ |
|
295 |
+ % (trendWindow[gStart], _AVERAGE_LINE_COLOR) |
|
296 |
+ elif trendLine == 2: |
|
287 | 297 |
strCmd += "LINE1:dSeries#0400ff " |
288 |
- strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE2:smoothed#000000 " \ |
|
289 |
- % trendWindow[gStart] |
|
298 |
+ strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE2:smoothed%s " \ |
|
299 |
+ % (trendWindow[gStart], _AVERAGE_LINE_COLOR) |
|
300 |
+ |
|
301 |
+ if alertLine != "": |
|
302 |
+ strCmd += "HRULE:%s#FF0000:Critical\ Low\ Voltage " % (alertLine) |
|
290 | 303 |
|
291 | 304 |
if verboseDebug: |
292 | 305 |
print "%s" % strCmd # DEBUG |
... | ... |
@@ -304,60 +317,68 @@ def createGraph(fileName, dataItem, gLabel, gTitle, gStart, |
304 | 317 |
print "rrdtool graph: %s\n" % result, |
305 | 318 |
return True |
306 | 319 |
|
307 |
-##end def |
|
320 |
+## end def |
|
308 | 321 |
|
309 | 322 |
def generateGraphs(): |
310 | 323 |
"""Generate graphs for display in html documents. |
311 | 324 |
Parameters: none |
312 | 325 |
Returns: nothing |
313 | 326 |
""" |
314 |
- autoScale = False |
|
315 | 327 |
|
316 | 328 |
# 24 hour stock charts |
317 | 329 |
|
318 |
- createGraph('24hr_current', 'CUR', 'mA', |
|
319 |
- 'Current\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 0, autoScale) |
|
320 |
- createGraph('24hr_voltage', 'VOLT', 'V', |
|
321 |
- 'Voltage\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 0, autoScale) |
|
322 |
- createGraph('24hr_power', 'PWR', 'mW', |
|
323 |
- 'Power\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 0, autoScale) |
|
330 |
+ createGraph('24hr_current', 'CUR', 'Amps', |
|
331 |
+ 'Current\ -\ Last\ 24\ Hours', 'end-1day', \ |
|
332 |
+ 0, 0, 2, 1000) |
|
333 |
+ createGraph('24hr_voltage', 'VOLT', 'Volts', |
|
334 |
+ 'Voltage\ -\ Last\ 24\ Hours', 'end-1day', \ |
|
335 |
+ 9, 15, 0, 1, True, 11) |
|
336 |
+ createGraph('24hr_power', 'PWR', 'Watts', |
|
337 |
+ 'Power\ -\ Last\ 24\ Hours', 'end-1day', \ |
|
338 |
+ 0, 0, 2, 1000) |
|
324 | 339 |
createGraph('24hr_battemp', 'BTMP', 'deg\ F', |
325 | 340 |
'Battery\ Temperature\ -\ Last\ 24\ Hours', 'end-1day', \ |
326 |
- 0, 0, 0, autoScale) |
|
341 |
+ 0, 0, 0) |
|
327 | 342 |
createGraph('24hr_ambtemp', 'ATMP', 'deg\ F', |
328 | 343 |
'Ambient\ Temperature\ -\ Last\ 24\ Hours', 'end-1day', \ |
329 |
- 0, 0, 0, autoScale) |
|
344 |
+ 0, 0, 0) |
|
330 | 345 |
|
331 | 346 |
# 4 week stock charts |
332 | 347 |
|
333 |
- createGraph('4wk_current', 'CUR', 'mA', |
|
334 |
- 'Current\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 0, autoScale) |
|
335 |
- createGraph('4wk_voltage', 'VOLT', 'V', |
|
336 |
- 'Voltage\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 0, autoScale) |
|
337 |
- createGraph('4wk_power', 'PWR', 'mW', |
|
338 |
- 'Power\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 0, autoScale) |
|
348 |
+ createGraph('4wk_current', 'CUR', 'Amps', |
|
349 |
+ 'Current\ -\ Last\ 4\ Weeks', 'end-4weeks', \ |
|
350 |
+ 0, 0, 2, 1000) |
|
351 |
+ createGraph('4wk_voltage', 'VOLT', 'Volts', |
|
352 |
+ 'Voltage\ -\ Last\ 4\ Weeks', 'end-4weeks', \ |
|
353 |
+ 9, 15, 0, 1, True, 11) |
|
354 |
+ createGraph('4wk_power', 'PWR', 'Watts', |
|
355 |
+ 'Power\ -\ Last\ 4\ Weeks', 'end-4weeks', \ |
|
356 |
+ 0, 0, 2, 1000) |
|
339 | 357 |
createGraph('4wk_battemp', 'BTMP', 'deg\ F', |
340 | 358 |
'Battery\ Temperature\ -\ Last\ 4\ Weeks', 'end-4weeks', \ |
341 |
- 0, 0, 2, autoScale) |
|
359 |
+ 0, 0, 2) |
|
342 | 360 |
createGraph('4wk_ambtemp', 'ATMP', 'deg\ F', |
343 | 361 |
'Ambient\ Temperature\ -\ Last\ 4\ Weeks', 'end-4weeks', \ |
344 |
- 0, 0, 0, autoScale) |
|
362 |
+ 0, 0, 2) |
|
345 | 363 |
|
346 | 364 |
# 12 month stock charts |
347 | 365 |
|
348 |
- createGraph('12m_current', 'CUR', 'mA', |
|
349 |
- 'Current\ -\ Past\ Year', 'end-12months', 0, 0, 0, autoScale) |
|
350 |
- createGraph('12m_voltage', 'VOLT', 'V', |
|
351 |
- 'Voltage\ -\ Past\ Year', 'end-12months', 0, 0, 0, autoScale) |
|
352 |
- createGraph('12m_power', 'PWR', 'mW', |
|
353 |
- 'Power\ -\ Past\ Year', 'end-12months', 0, 0, 0, autoScale) |
|
366 |
+ createGraph('12m_current', 'CUR', 'Amps', |
|
367 |
+ 'Current\ -\ Past\ Year', 'end-12months', \ |
|
368 |
+ 0, 0, 2, 1000) |
|
369 |
+ createGraph('12m_voltage', 'VOLT', 'Volts', |
|
370 |
+ 'Voltage\ -\ Past\ Year', 'end-12months', \ |
|
371 |
+ 9, 15, 0, 1, True, 11) |
|
372 |
+ createGraph('12m_power', 'PWR', 'Watts', |
|
373 |
+ 'Power\ -\ Past\ Year', 'end-12months', \ |
|
374 |
+ 0, 0, 2, 1000) |
|
354 | 375 |
createGraph('12m_battemp', 'BTMP', 'deg\ F', |
355 | 376 |
'Battery\ Temperature\ -\ Past\ Year', 'end-12months', \ |
356 |
- 0, 0, 0, autoScale) |
|
377 |
+ 0, 0, 2) |
|
357 | 378 |
createGraph('12m_ambtemp', 'ATMP', 'deg\ F', |
358 | 379 |
'Ambient\ Temperature\ -\ Past\ Year', 'end-12months', \ |
359 |
- 0, 0, 0, autoScale) |
|
360 |
-##end def |
|
380 |
+ 0, 0, 2) |
|
381 |
+## end def |
|
361 | 382 |
|
362 | 383 |
def getCLarguments(): |
363 | 384 |
"""Get command line arguments. There are three possible arguments |
... | ... |
@@ -437,6 +458,7 @@ def main(): |
437 | 458 |
# If get data successful, write data to data files. |
438 | 459 |
if result: |
439 | 460 |
result = writeOutputDataFile(dData) |
461 |
+ pass |
|
440 | 462 |
|
441 | 463 |
# At the rrdtool database update interval, update the database. |
442 | 464 |
if currentTime - lastDatabaseUpdateTime > \ |
... | ... |
@@ -445,13 +467,14 @@ def main(): |
445 | 467 |
## Update the round robin database with the parsed data. |
446 | 468 |
if result: |
447 | 469 |
updateDatabase(dData) |
470 |
+ pass |
|
448 | 471 |
|
449 | 472 |
# At the chart generation interval, generate charts. |
450 | 473 |
if currentTime - lastChartUpdateTime > chartUpdateInterval: |
451 | 474 |
lastChartUpdateTime = currentTime |
452 | 475 |
p = multiprocessing.Process(target=generateGraphs, args=()) |
453 | 476 |
p.start() |
454 |
- |
|
477 |
+ |
|
455 | 478 |
# Relinquish processing back to the operating system until |
456 | 479 |
# the next update interval. |
457 | 480 |
|
... | ... |
@@ -153,8 +153,8 @@ def getSensorData(dData): |
153 | 153 |
dData["current"] = pwr.getCurrent() |
154 | 154 |
dData["voltage"] = pwr.getVoltage() |
155 | 155 |
dData["power"] = pwr.getPower() |
156 |
- dData["battemp"] = btmp.getTempC() |
|
157 |
- dData["ambtemp"] = atmp.getTempC() |
|
156 |
+ dData["battemp"] = btmp.getTempF() |
|
157 |
+ dData["ambtemp"] = atmp.getTempF() |
|
158 | 158 |
|
159 | 159 |
except Exception, exError: |
160 | 160 |
print "%s sensor error: %s" % (getTimeStamp(), exError) |
... | ... |
@@ -281,11 +281,11 @@ def createGraph(fileName, dataItem, gLabel, gTitle, gStart, |
281 | 281 |
if addTrend == 0: |
282 | 282 |
strCmd += "LINE1:dSeries#0400ff " |
283 | 283 |
elif addTrend == 1: |
284 |
- strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \ |
|
284 |
+ strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE2:smoothed#000000 " \ |
|
285 | 285 |
% trendWindow[gStart] |
286 | 286 |
elif addTrend == 2: |
287 | 287 |
strCmd += "LINE1:dSeries#0400ff " |
288 |
- strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \ |
|
288 |
+ strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE2:smoothed#000000 " \ |
|
289 | 289 |
% trendWindow[gStart] |
290 | 290 |
|
291 | 291 |
if verboseDebug: |
... | ... |
@@ -316,47 +316,47 @@ def generateGraphs(): |
316 | 316 |
# 24 hour stock charts |
317 | 317 |
|
318 | 318 |
createGraph('24hr_current', 'CUR', 'mA', |
319 |
- 'Current\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale) |
|
319 |
+ 'Current\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 0, autoScale) |
|
320 | 320 |
createGraph('24hr_voltage', 'VOLT', 'V', |
321 |
- 'Voltage\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale) |
|
321 |
+ 'Voltage\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 0, autoScale) |
|
322 | 322 |
createGraph('24hr_power', 'PWR', 'mW', |
323 |
- 'Power\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale) |
|
324 |
- createGraph('24hr_battemp', 'BTMP', 'deg\ C', |
|
323 |
+ 'Power\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 0, autoScale) |
|
324 |
+ createGraph('24hr_battemp', 'BTMP', 'deg\ F', |
|
325 | 325 |
'Battery\ Temperature\ -\ Last\ 24\ Hours', 'end-1day', \ |
326 |
- 0, 0, 2, autoScale) |
|
327 |
- createGraph('24hr_ambtemp', 'ATMP', 'deg\ C', |
|
326 |
+ 0, 0, 0, autoScale) |
|
327 |
+ createGraph('24hr_ambtemp', 'ATMP', 'deg\ F', |
|
328 | 328 |
'Ambient\ Temperature\ -\ Last\ 24\ Hours', 'end-1day', \ |
329 |
- 0, 0, 2, autoScale) |
|
329 |
+ 0, 0, 0, autoScale) |
|
330 | 330 |
|
331 | 331 |
# 4 week stock charts |
332 | 332 |
|
333 | 333 |
createGraph('4wk_current', 'CUR', 'mA', |
334 |
- 'Current\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale) |
|
334 |
+ 'Current\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 0, autoScale) |
|
335 | 335 |
createGraph('4wk_voltage', 'VOLT', 'V', |
336 |
- 'Voltage\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale) |
|
336 |
+ 'Voltage\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 0, autoScale) |
|
337 | 337 |
createGraph('4wk_power', 'PWR', 'mW', |
338 |
- 'Power\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale) |
|
339 |
- createGraph('4wk_battemp', 'BTMP', 'deg\ C', |
|
338 |
+ 'Power\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 0, autoScale) |
|
339 |
+ createGraph('4wk_battemp', 'BTMP', 'deg\ F', |
|
340 | 340 |
'Battery\ Temperature\ -\ Last\ 4\ Weeks', 'end-4weeks', \ |
341 | 341 |
0, 0, 2, autoScale) |
342 |
- createGraph('4wk_ambtemp', 'ATMP', 'deg\ C', |
|
342 |
+ createGraph('4wk_ambtemp', 'ATMP', 'deg\ F', |
|
343 | 343 |
'Ambient\ Temperature\ -\ Last\ 4\ Weeks', 'end-4weeks', \ |
344 |
- 0, 0, 2, autoScale) |
|
344 |
+ 0, 0, 0, autoScale) |
|
345 | 345 |
|
346 | 346 |
# 12 month stock charts |
347 | 347 |
|
348 | 348 |
createGraph('12m_current', 'CUR', 'mA', |
349 |
- 'Current\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale) |
|
349 |
+ 'Current\ -\ Past\ Year', 'end-12months', 0, 0, 0, autoScale) |
|
350 | 350 |
createGraph('12m_voltage', 'VOLT', 'V', |
351 |
- 'Voltage\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale) |
|
351 |
+ 'Voltage\ -\ Past\ Year', 'end-12months', 0, 0, 0, autoScale) |
|
352 | 352 |
createGraph('12m_power', 'PWR', 'mW', |
353 |
- 'Power\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale) |
|
354 |
- createGraph('12m_battemp', 'BTMP', 'deg\ C', |
|
353 |
+ 'Power\ -\ Past\ Year', 'end-12months', 0, 0, 0, autoScale) |
|
354 |
+ createGraph('12m_battemp', 'BTMP', 'deg\ F', |
|
355 | 355 |
'Battery\ Temperature\ -\ Past\ Year', 'end-12months', \ |
356 |
- 0, 0, 2, autoScale) |
|
357 |
- createGraph('12m_ambtemp', 'ATMP', 'deg\ C', |
|
356 |
+ 0, 0, 0, autoScale) |
|
357 |
+ createGraph('12m_ambtemp', 'ATMP', 'deg\ F', |
|
358 | 358 |
'Ambient\ Temperature\ -\ Past\ Year', 'end-12months', \ |
359 |
- 0, 0, 2, autoScale) |
|
359 |
+ 0, 0, 0, autoScale) |
|
360 | 360 |
##end def |
361 | 361 |
|
362 | 362 |
def getCLarguments(): |
1 | 1 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,477 @@ |
1 |
+#!/usr/bin/python2 -u |
|
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 |
+# |
|
5 |
+# Module: nodepowerAgent.py |
|
6 |
+# |
|
7 |
+# Description: This module acts as an agent between the mesh network and |
|
8 |
+# node power and enviromental sensors. The agent periodically polls the |
|
9 |
+# sensors and processes the data returned from the sensors, including |
|
10 |
+# - conversion of data items |
|
11 |
+# - update a round robin (rrdtool) database with the sensor data |
|
12 |
+# - periodically generate graphic charts for display in html documents |
|
13 |
+# - write the processed node status to a JSON file for use by html |
|
14 |
+# documents |
|
15 |
+# |
|
16 |
+# Copyright 2021 Jeff Owrey |
|
17 |
+# This program is free software: you can redistribute it and/or modify |
|
18 |
+# it under the terms of the GNU General Public License as published by |
|
19 |
+# the Free Software Foundation, either version 3 of the License, or |
|
20 |
+# (at your option) any later version. |
|
21 |
+# |
|
22 |
+# This program is distributed in the hope that it will be useful, |
|
23 |
+# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
24 |
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
25 |
+# GNU General Public License for more details. |
|
26 |
+# |
|
27 |
+# You should have received a copy of the GNU General Public License |
|
28 |
+# along with this program. If not, see http://www.gnu.org/license. |
|
29 |
+# |
|
30 |
+# Revision History |
|
31 |
+# * v10 released 01 June 2021 by J L Owrey; first release |
|
32 |
+# |
|
33 |
+#2345678901234567890123456789012345678901234567890123456789012345678901234567890 |
|
34 |
+ |
|
35 |
+# Import required python libraries. |
|
36 |
+import os |
|
37 |
+import sys |
|
38 |
+import signal |
|
39 |
+import subprocess |
|
40 |
+import multiprocessing |
|
41 |
+import time |
|
42 |
+ |
|
43 |
+# Import sensor libraries. |
|
44 |
+import ina260 # power sensor |
|
45 |
+import tmp102 # temperature sensor |
|
46 |
+ |
|
47 |
+ ### SENSOR BUS ADDRESSES ### |
|
48 |
+ |
|
49 |
+# Set bus addresses of sensors |
|
50 |
+_PWR_SENSOR_ADDR = 0X40 |
|
51 |
+_BAT_TMP_SENSOR_ADDR = 0x48 |
|
52 |
+_AMB_TMP_SENSOR_ADDR = 0x4B |
|
53 |
+# Set bus selector |
|
54 |
+_BUS_SEL = 1 |
|
55 |
+ |
|
56 |
+ ### FILE AND FOLDER LOCATIONS ### |
|
57 |
+ |
|
58 |
+_USER = os.environ['USER'] |
|
59 |
+# folder for containing dynamic data objects |
|
60 |
+_DOCROOT_PATH = "/home/%s/public_html/power/" % _USER |
|
61 |
+# folder for charts and output data file |
|
62 |
+_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/" |
|
63 |
+# location of data output file |
|
64 |
+_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/powerData.js" |
|
65 |
+# database that stores node data |
|
66 |
+_RRD_FILE = "/home/%s/database/powerData.rrd" % _USER |
|
67 |
+ |
|
68 |
+ ### GLOBAL CONSTANTS ### |
|
69 |
+ |
|
70 |
+# rrdtool database update interval in seconds |
|
71 |
+_DATABASE_UPDATE_INTERVAL = 30 |
|
72 |
+# sensor data request interval in seconds |
|
73 |
+_DEFAULT_DATA_REQUEST_INTERVAL = 2 |
|
74 |
+# chart update interval in seconds |
|
75 |
+_CHART_UPDATE_INTERVAL = 600 |
|
76 |
+# standard chart width in pixels |
|
77 |
+_CHART_WIDTH = 600 |
|
78 |
+# standard chart height in pixels |
|
79 |
+_CHART_HEIGHT = 150 |
|
80 |
+ |
|
81 |
+ ### GLOBAL VARIABLES ### |
|
82 |
+ |
|
83 |
+# turn on or off of verbose debugging information |
|
84 |
+debugOption = False |
|
85 |
+verboseDebug = False |
|
86 |
+ |
|
87 |
+# frequency of data requests to sensors |
|
88 |
+dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL |
|
89 |
+# chart update interval |
|
90 |
+chartUpdateInterval = _CHART_UPDATE_INTERVAL |
|
91 |
+# last node request time |
|
92 |
+lastDataPointTime = -1 |
|
93 |
+ |
|
94 |
+# Define each sensor. This also initialzes each sensor. |
|
95 |
+pwr = ina260.ina260(_PWR_SENSOR_ADDR, _BUS_SEL) |
|
96 |
+btmp = tmp102.tmp102(_BAT_TMP_SENSOR_ADDR, _BUS_SEL) |
|
97 |
+atmp = tmp102.tmp102(_AMB_TMP_SENSOR_ADDR, _BUS_SEL) |
|
98 |
+ |
|
99 |
+ ### PRIVATE METHODS ### |
|
100 |
+ |
|
101 |
+def getTimeStamp(): |
|
102 |
+ """ |
|
103 |
+ Set the error message time stamp to the local system time. |
|
104 |
+ Parameters: none |
|
105 |
+ Returns: string containing the time stamp |
|
106 |
+ """ |
|
107 |
+ return time.strftime( "%m/%d/%Y %T", time.localtime() ) |
|
108 |
+##end def |
|
109 |
+ |
|
110 |
+def getEpochSeconds(sTime): |
|
111 |
+ """Convert the time stamp supplied in the weather data string |
|
112 |
+ to seconds since 1/1/1970 00:00:00. |
|
113 |
+ Parameters: |
|
114 |
+ sTime - the time stamp to be converted must be formatted |
|
115 |
+ as %m/%d/%Y %H:%M:%S |
|
116 |
+ Returns: epoch seconds |
|
117 |
+ """ |
|
118 |
+ try: |
|
119 |
+ t_sTime = time.strptime(sTime, '%m/%d/%Y %H:%M:%S') |
|
120 |
+ except Exception, exError: |
|
121 |
+ print '%s getEpochSeconds: %s' % (getTimeStamp(), exError) |
|
122 |
+ return None |
|
123 |
+ tSeconds = int(time.mktime(t_sTime)) |
|
124 |
+ return tSeconds |
|
125 |
+##end def |
|
126 |
+ |
|
127 |
+def terminateAgentProcess(signal, frame): |
|
128 |
+ """Send a message to log when the agent process gets killed |
|
129 |
+ by the operating system. Inform downstream clients |
|
130 |
+ by removing input and output data files. |
|
131 |
+ Parameters: |
|
132 |
+ signal, frame - dummy parameters |
|
133 |
+ Returns: nothing |
|
134 |
+ """ |
|
135 |
+ # Inform downstream clients by removing output data file. |
|
136 |
+ if os.path.exists(_OUTPUT_DATA_FILE): |
|
137 |
+ os.remove(_OUTPUT_DATA_FILE) |
|
138 |
+ print '%s terminating npw agent process' % \ |
|
139 |
+ (getTimeStamp()) |
|
140 |
+ sys.exit(0) |
|
141 |
+##end def |
|
142 |
+ |
|
143 |
+ ### PUBLIC METHODS ### |
|
144 |
+ |
|
145 |
+def getSensorData(dData): |
|
146 |
+ """Poll sensors for data. |
|
147 |
+ Parameters: none |
|
148 |
+ Returns: a string containing the node signal data if successful, |
|
149 |
+ or None if not successful |
|
150 |
+ """ |
|
151 |
+ try: |
|
152 |
+ dData["time"] = getTimeStamp() |
|
153 |
+ dData["current"] = pwr.getCurrent() |
|
154 |
+ dData["voltage"] = pwr.getVoltage() |
|
155 |
+ dData["power"] = pwr.getPower() |
|
156 |
+ dData["battemp"] = btmp.getTempC() |
|
157 |
+ dData["ambtemp"] = atmp.getTempC() |
|
158 |
+ |
|
159 |
+ except Exception, exError: |
|
160 |
+ print "%s sensor error: %s" % (getTimeStamp(), exError) |
|
161 |
+ return False |
|
162 |
+ |
|
163 |
+ return True |
|
164 |
+##end def |
|
165 |
+ |
|
166 |
+def updateDatabase(dData): |
|
167 |
+ """ |
|
168 |
+ Update the rrdtool database by executing an rrdtool system command. |
|
169 |
+ Format the command using the data extracted from the sensors. |
|
170 |
+ Parameters: dData - dictionary object containing data items to be |
|
171 |
+ written to the rr database file |
|
172 |
+ Returns: True if successful, False otherwise |
|
173 |
+ """ |
|
174 |
+ |
|
175 |
+ epochTime = getEpochSeconds(dData['time']) |
|
176 |
+ |
|
177 |
+ # Format the rrdtool update command. |
|
178 |
+ strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s" |
|
179 |
+ strCmd = strFmt % (_RRD_FILE, epochTime, dData['current'], \ |
|
180 |
+ dData['voltage'], dData['power'], dData['battemp'], \ |
|
181 |
+ dData['ambtemp']) |
|
182 |
+ |
|
183 |
+ if debugOption: |
|
184 |
+ print "%s" % strCmd # DEBUG |
|
185 |
+ |
|
186 |
+ # Run the command as a subprocess. |
|
187 |
+ try: |
|
188 |
+ subprocess.check_output(strCmd, shell=True, \ |
|
189 |
+ stderr=subprocess.STDOUT) |
|
190 |
+ except subprocess.CalledProcessError, exError: |
|
191 |
+ print "%s: rrdtool update failed: %s" % \ |
|
192 |
+ (getTimeStamp(), exError.output) |
|
193 |
+ return False |
|
194 |
+ |
|
195 |
+ return True |
|
196 |
+##end def |
|
197 |
+ |
|
198 |
+def writeOutputDataFile(dData): |
|
199 |
+ """Write node data items to the output data file, formatted as |
|
200 |
+ a Javascript file. This file may then be accessed and used by |
|
201 |
+ by downstream clients, for instance, in HTML documents. |
|
202 |
+ Parameters: |
|
203 |
+ sData - a string object containing the data to be written |
|
204 |
+ to the output data file |
|
205 |
+ Returns: True if successful, False otherwise |
|
206 |
+ """ |
|
207 |
+ # Write a JSON formatted file for use by html clients. The following |
|
208 |
+ # data items are sent to the client file. |
|
209 |
+ # * The last database update date and time |
|
210 |
+ # * The data request interval |
|
211 |
+ # * The sensor values |
|
212 |
+ |
|
213 |
+ # Create a JSON formatted string from the sensor data. |
|
214 |
+ sData = "[{\"period\":\"%s\", " % \ |
|
215 |
+ (chartUpdateInterval) |
|
216 |
+ for key in dData: |
|
217 |
+ sData += '\"%s\":\"%s\", ' % (key, dData[key]) |
|
218 |
+ sData = sData[:-2] + '}]\n' |
|
219 |
+ |
|
220 |
+ # Write the JSON formatted data to the output data file. |
|
221 |
+ try: |
|
222 |
+ fc = open(_OUTPUT_DATA_FILE, "w") |
|
223 |
+ fc.write(sData) |
|
224 |
+ fc.close() |
|
225 |
+ except Exception, exError: |
|
226 |
+ print "%s write output file failed: %s" % \ |
|
227 |
+ (getTimeStamp(), exError) |
|
228 |
+ return False |
|
229 |
+ |
|
230 |
+ if verboseDebug: |
|
231 |
+ print sData[:-1] |
|
232 |
+ if debugOption: |
|
233 |
+ print "writing output data file: %d bytes" % len(sData) |
|
234 |
+ |
|
235 |
+ return True |
|
236 |
+## end def |
|
237 |
+ |
|
238 |
+def createGraph(fileName, dataItem, gLabel, gTitle, gStart, |
|
239 |
+ lower, upper, addTrend, autoScale): |
|
240 |
+ """Uses rrdtool to create a graph of specified node data item. |
|
241 |
+ Parameters: |
|
242 |
+ fileName - name of file containing the graph |
|
243 |
+ dataItem - data item to be graphed |
|
244 |
+ gLabel - string containing a graph label for the data item |
|
245 |
+ gTitle - string containing a title for the graph |
|
246 |
+ gStart - beginning time of the graphed data |
|
247 |
+ lower - lower bound for graph ordinate #NOT USED |
|
248 |
+ upper - upper bound for graph ordinate #NOT USED |
|
249 |
+ addTrend - 0, show only graph data |
|
250 |
+ 1, show only a trend line |
|
251 |
+ 2, show a trend line and the graph data |
|
252 |
+ autoScale - if True, then use vertical axis auto scaling |
|
253 |
+ (lower and upper parameters are ignored), otherwise use |
|
254 |
+ lower and upper parameters to set vertical axis scale |
|
255 |
+ Returns: True if successful, False otherwise |
|
256 |
+ """ |
|
257 |
+ gPath = _CHARTS_DIRECTORY + fileName + ".png" |
|
258 |
+ trendWindow = { 'end-1day': 7200, |
|
259 |
+ 'end-4weeks': 172800, |
|
260 |
+ 'end-12months': 604800 } |
|
261 |
+ |
|
262 |
+ # Format the rrdtool graph command. |
|
263 |
+ |
|
264 |
+ # Set chart start time, height, and width. |
|
265 |
+ strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \ |
|
266 |
+ % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT) |
|
267 |
+ |
|
268 |
+ # Set the range and scaling of the chart y-axis. |
|
269 |
+ if lower < upper: |
|
270 |
+ strCmd += "-l %s -u %s -r " % (lower, upper) |
|
271 |
+ elif autoScale: |
|
272 |
+ strCmd += "-A " |
|
273 |
+ strCmd += "-Y " |
|
274 |
+ |
|
275 |
+ # Set the chart ordinate label and chart title. |
|
276 |
+ strCmd += "-v %s -t %s " % (gLabel, gTitle) |
|
277 |
+ |
|
278 |
+ # Show the data, or a moving average trend line over |
|
279 |
+ # the data, or both. |
|
280 |
+ strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem) |
|
281 |
+ if addTrend == 0: |
|
282 |
+ strCmd += "LINE1:dSeries#0400ff " |
|
283 |
+ elif addTrend == 1: |
|
284 |
+ strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \ |
|
285 |
+ % trendWindow[gStart] |
|
286 |
+ elif addTrend == 2: |
|
287 |
+ strCmd += "LINE1:dSeries#0400ff " |
|
288 |
+ strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \ |
|
289 |
+ % trendWindow[gStart] |
|
290 |
+ |
|
291 |
+ if verboseDebug: |
|
292 |
+ print "%s" % strCmd # DEBUG |
|
293 |
+ |
|
294 |
+ # Run the formatted rrdtool command as a subprocess. |
|
295 |
+ try: |
|
296 |
+ result = subprocess.check_output(strCmd, \ |
|
297 |
+ stderr=subprocess.STDOUT, \ |
|
298 |
+ shell=True) |
|
299 |
+ except subprocess.CalledProcessError, exError: |
|
300 |
+ print "rrdtool graph failed: %s" % (exError.output) |
|
301 |
+ return False |
|
302 |
+ |
|
303 |
+ if debugOption: |
|
304 |
+ print "rrdtool graph: %s\n" % result, |
|
305 |
+ return True |
|
306 |
+ |
|
307 |
+##end def |
|
308 |
+ |
|
309 |
+def generateGraphs(): |
|
310 |
+ """Generate graphs for display in html documents. |
|
311 |
+ Parameters: none |
|
312 |
+ Returns: nothing |
|
313 |
+ """ |
|
314 |
+ autoScale = False |
|
315 |
+ |
|
316 |
+ # 24 hour stock charts |
|
317 |
+ |
|
318 |
+ createGraph('24hr_current', 'CUR', 'mA', |
|
319 |
+ 'Current\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale) |
|
320 |
+ createGraph('24hr_voltage', 'VOLT', 'V', |
|
321 |
+ 'Voltage\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale) |
|
322 |
+ createGraph('24hr_power', 'PWR', 'mW', |
|
323 |
+ 'Power\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale) |
|
324 |
+ createGraph('24hr_battemp', 'BTMP', 'deg\ C', |
|
325 |
+ 'Battery\ Temperature\ -\ Last\ 24\ Hours', 'end-1day', \ |
|
326 |
+ 0, 0, 2, autoScale) |
|
327 |
+ createGraph('24hr_ambtemp', 'ATMP', 'deg\ C', |
|
328 |
+ 'Ambient\ Temperature\ -\ Last\ 24\ Hours', 'end-1day', \ |
|
329 |
+ 0, 0, 2, autoScale) |
|
330 |
+ |
|
331 |
+ # 4 week stock charts |
|
332 |
+ |
|
333 |
+ createGraph('4wk_current', 'CUR', 'mA', |
|
334 |
+ 'Current\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale) |
|
335 |
+ createGraph('4wk_voltage', 'VOLT', 'V', |
|
336 |
+ 'Voltage\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale) |
|
337 |
+ createGraph('4wk_power', 'PWR', 'mW', |
|
338 |
+ 'Power\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale) |
|
339 |
+ createGraph('4wk_battemp', 'BTMP', 'deg\ C', |
|
340 |
+ 'Battery\ Temperature\ -\ Last\ 4\ Weeks', 'end-4weeks', \ |
|
341 |
+ 0, 0, 2, autoScale) |
|
342 |
+ createGraph('4wk_ambtemp', 'ATMP', 'deg\ C', |
|
343 |
+ 'Ambient\ Temperature\ -\ Last\ 4\ Weeks', 'end-4weeks', \ |
|
344 |
+ 0, 0, 2, autoScale) |
|
345 |
+ |
|
346 |
+ # 12 month stock charts |
|
347 |
+ |
|
348 |
+ createGraph('12m_current', 'CUR', 'mA', |
|
349 |
+ 'Current\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale) |
|
350 |
+ createGraph('12m_voltage', 'VOLT', 'V', |
|
351 |
+ 'Voltage\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale) |
|
352 |
+ createGraph('12m_power', 'PWR', 'mW', |
|
353 |
+ 'Power\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale) |
|
354 |
+ createGraph('12m_battemp', 'BTMP', 'deg\ C', |
|
355 |
+ 'Battery\ Temperature\ -\ Past\ Year', 'end-12months', \ |
|
356 |
+ 0, 0, 2, autoScale) |
|
357 |
+ createGraph('12m_ambtemp', 'ATMP', 'deg\ C', |
|
358 |
+ 'Ambient\ Temperature\ -\ Past\ Year', 'end-12months', \ |
|
359 |
+ 0, 0, 2, autoScale) |
|
360 |
+##end def |
|
361 |
+ |
|
362 |
+def getCLarguments(): |
|
363 |
+ """Get command line arguments. There are three possible arguments |
|
364 |
+ -d turns on debug mode |
|
365 |
+ -v turns on verbose debug mode |
|
366 |
+ -t sets the sensor query interval |
|
367 |
+ Returns: nothing |
|
368 |
+ """ |
|
369 |
+ global debugOption, verboseDebug, dataRequestInterval |
|
370 |
+ |
|
371 |
+ index = 1 |
|
372 |
+ while index < len(sys.argv): |
|
373 |
+ if sys.argv[index] == '-d': |
|
374 |
+ debugOption = True |
|
375 |
+ elif sys.argv[index] == '-v': |
|
376 |
+ debugOption = True |
|
377 |
+ verboseDebug = True |
|
378 |
+ elif sys.argv[index] == '-p': |
|
379 |
+ try: |
|
380 |
+ dataRequestInterval = abs(int(sys.argv[index + 1])) |
|
381 |
+ except: |
|
382 |
+ print "invalid polling period" |
|
383 |
+ exit(-1) |
|
384 |
+ index += 1 |
|
385 |
+ else: |
|
386 |
+ cmd_name = sys.argv[0].split('/') |
|
387 |
+ print "Usage: %s [-d] [-v] [-p seconds]" % cmd_name[-1] |
|
388 |
+ exit(-1) |
|
389 |
+ index += 1 |
|
390 |
+##end def |
|
391 |
+ |
|
392 |
+def main(): |
|
393 |
+ """Handles timing of events and acts as executive routine managing |
|
394 |
+ all other functions. |
|
395 |
+ Parameters: none |
|
396 |
+ Returns: nothing |
|
397 |
+ """ |
|
398 |
+ global dataRequestInterval |
|
399 |
+ |
|
400 |
+ signal.signal(signal.SIGTERM, terminateAgentProcess) |
|
401 |
+ |
|
402 |
+ print '%s starting up node power agent process' % \ |
|
403 |
+ (getTimeStamp()) |
|
404 |
+ |
|
405 |
+ # last time output JSON file updated |
|
406 |
+ lastDataRequestTime = -1 |
|
407 |
+ # last time charts generated |
|
408 |
+ lastChartUpdateTime = - 1 |
|
409 |
+ # last time the rrdtool database updated |
|
410 |
+ lastDatabaseUpdateTime = -1 |
|
411 |
+ |
|
412 |
+ ## Get command line arguments. |
|
413 |
+ getCLarguments() |
|
414 |
+ |
|
415 |
+ ## Exit with error if rrdtool database does not exist. |
|
416 |
+ if not os.path.exists(_RRD_FILE): |
|
417 |
+ print 'rrdtool database does not exist\n' \ |
|
418 |
+ 'use createArednsigRrd script to ' \ |
|
419 |
+ 'create rrdtool database\n' |
|
420 |
+ exit(1) |
|
421 |
+ |
|
422 |
+ ## main loop |
|
423 |
+ while True: |
|
424 |
+ |
|
425 |
+ currentTime = time.time() # get current time in seconds |
|
426 |
+ |
|
427 |
+ # Every web update interval request data from the aredn |
|
428 |
+ # node and process the received data. |
|
429 |
+ if currentTime - lastDataRequestTime > dataRequestInterval: |
|
430 |
+ lastDataRequestTime = currentTime |
|
431 |
+ dData = {} |
|
432 |
+ result = True |
|
433 |
+ |
|
434 |
+ # Get the data from the sensors. |
|
435 |
+ result =getSensorData(dData) |
|
436 |
+ |
|
437 |
+ # If get data successful, write data to data files. |
|
438 |
+ if result: |
|
439 |
+ result = writeOutputDataFile(dData) |
|
440 |
+ |
|
441 |
+ # At the rrdtool database update interval, update the database. |
|
442 |
+ if currentTime - lastDatabaseUpdateTime > \ |
|
443 |
+ _DATABASE_UPDATE_INTERVAL: |
|
444 |
+ lastDatabaseUpdateTime = currentTime |
|
445 |
+ ## Update the round robin database with the parsed data. |
|
446 |
+ if result: |
|
447 |
+ updateDatabase(dData) |
|
448 |
+ |
|
449 |
+ # At the chart generation interval, generate charts. |
|
450 |
+ if currentTime - lastChartUpdateTime > chartUpdateInterval: |
|
451 |
+ lastChartUpdateTime = currentTime |
|
452 |
+ p = multiprocessing.Process(target=generateGraphs, args=()) |
|
453 |
+ p.start() |
|
454 |
+ |
|
455 |
+ # Relinquish processing back to the operating system until |
|
456 |
+ # the next update interval. |
|
457 |
+ |
|
458 |
+ elapsedTime = time.time() - currentTime |
|
459 |
+ if debugOption: |
|
460 |
+ if result: |
|
461 |
+ print "%s update successful:" % getTimeStamp(), |
|
462 |
+ else: |
|
463 |
+ print "%s update failed:" % getTimeStamp(), |
|
464 |
+ print "%6f seconds processing time\n" % elapsedTime |
|
465 |
+ remainingTime = dataRequestInterval - elapsedTime |
|
466 |
+ if remainingTime > 0.0: |
|
467 |
+ time.sleep(remainingTime) |
|
468 |
+ ## end while |
|
469 |
+ return |
|
470 |
+## end def |
|
471 |
+ |
|
472 |
+if __name__ == '__main__': |
|
473 |
+ try: |
|
474 |
+ main() |
|
475 |
+ except KeyboardInterrupt: |
|
476 |
+ print '\n', |
|
477 |
+ terminateAgentProcess('KeyboardInterrupt','Module') |