Browse code

add sms alert functionality

Gandolf authored on 12/09/2021 02:11:18
Showing 1 changed files
... ...
@@ -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()
Browse code

added main_loop

Gandolf authored on 08/20/2021 21:22:01
Showing 1 changed files
... ...
@@ -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__':
Browse code

minor revision

Gandolf authored on 07/12/2021 20:39:47
Showing 1 changed files
... ...
@@ -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:
Browse code

minor revision

Gandolf authored on 07/06/2021 21:04:25
Showing 1 changed files
... ...
@@ -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:
Browse code

minor revision

Gandolf authored on 07/03/2021 01:42:15
Showing 1 changed files
... ...
@@ -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
Browse code

minor revisions

Gandolf authored on 06/23/2021 01:42:11
Showing 1 changed files
... ...
@@ -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)
Browse code

added class methods for configuring sensor; added exception catching for json formatting code

Gandolf authored on 06/20/2021 00:40:59
Showing 1 changed files
... ...
@@ -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()
Browse code

minor revisions

Gandolf authored on 06/08/2021 19:02:58
Showing 1 changed files
... ...
@@ -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
Browse code

modify charts

Gandolf authored on 06/07/2021 20:50:02
Showing 1 changed files
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
 
Browse code

base release

Gandolf authored on 06/02/2021 02:54:40
Showing 1 changed files
... ...
@@ -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():
Browse code

Initial release

Gandolf authored on 05/29/2021 20:13:39
Showing 1 changed files
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')