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: