Browse code

reorg_20202027

Gandolf authored on 10/27/2020 20:25:34
Showing 1 changed files
1 1
deleted file mode 100755
... ...
@@ -1,590 +0,0 @@
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: arednsigAgent.py
6
-#
7
-# Description: This module acts as an agent between the aredn node
8
-# and aredn mest services.  The agent periodically sends an http
9
-# request to the aredn node, processes the response from
10
-# the node, and performs a number of operations:
11
-#     - conversion of data items
12
-#     - update a round robin (rrdtool) database with the node data
13
-#     - periodically generate graphic charts for display in html documents
14
-#     - write the processed node status to a JSON file for use by html
15
-#       documents
16
-#
17
-# Copyright 2020 Jeff Owrey
18
-#    This program is free software: you can redistribute it and/or modify
19
-#    it under the terms of the GNU General Public License as published by
20
-#    the Free Software Foundation, either version 3 of the License, or
21
-#    (at your option) any later version.
22
-#
23
-#    This program is distributed in the hope that it will be useful,
24
-#    but WITHOUT ANY WARRANTY; without even the implied warranty of
25
-#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26
-#    GNU General Public License for more details.
27
-#
28
-#    You should have received a copy of the GNU General Public License
29
-#    along with this program.  If not, see http://www.gnu.org/license.
30
-#
31
-# Revision History
32
-#   * v20 released 11 Jan 2020 by J L Owrey; first release
33
-#   * v21 released 13 Feb 2020 by J L Owrey; fixed bug occuring when node
34
-#     powers on and signal data memory is empty.  Data points with N/A data
35
-#     are discarded.
36
-#   * v22 released 31 Mar 2020 by J L Owrey; upgraded for compatibility with
37
-#     Aredn firmware version 3.20.3.0.  This agent now downloads the node's
38
-#     status page and parsed the signal data from the html.
39
-#
40
-#2345678901234567890123456789012345678901234567890123456789012345678901234567890
41
-
42
-import os
43
-import urllib2
44
-import sys
45
-import signal
46
-import subprocess
47
-import multiprocessing
48
-import time
49
-
50
-_USER = os.environ['USER']
51
-
52
-   ### DEFAULT AREDN NODE URL ###
53
-
54
-# set url of the aredn node
55
-
56
-_DEFAULT_AREDN_NODE_URL = "http://localnode:8080/cgi-bin/status"
57
-
58
-    ### FILE AND FOLDER LOCATIONS ###
59
-
60
-# folder for containing dynamic data objects
61
-_DOCROOT_PATH = "/home/%s/public_html/arednsig/" % _USER
62
-# folder for charts and output data file
63
-_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/"
64
-# location of data output file
65
-_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/arednsigOutputData.js"
66
-# dummy output data file
67
-_DUMMY_OUTPUT_FILE = _DOCROOT_PATH + "dynamic/nodeOnline.js"
68
-# database that stores node data
69
-_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
70
-
71
-    ### GLOBAL CONSTANTS ###
72
-
73
-# interval in seconds between data requests to the aredn node
74
-_DEFAULT_DATA_REQUEST_INTERVAL = 60
75
-# chart update interval in minutes
76
-_CHART_UPDATE_INTERVAL = 600
77
-
78
-# number seconds to wait for a response to HTTP request
79
-_HTTP_REQUEST_TIMEOUT = 10
80
-# max number of failed data requests allowed
81
-_MAX_FAILED_DATA_REQUESTS = 0
82
-# standard chart width in pixels
83
-_CHART_WIDTH = 600
84
-# standard chart height in pixels
85
-_CHART_HEIGHT = 150
86
-# Set this to True only if this server is intended to relay raw
87
-# node data to a mirror server.
88
-_RELAY_SERVER = False
89
-
90
-   ### GLOBAL VARIABLES ###
91
-
92
-# turn on or off of verbose debugging information
93
-debugOption = False
94
-verboseDebug = False
95
-
96
-# The following two items are used for detecting system faults
97
-# and aredn node online or offline status.
98
-
99
-# count of failed attempts to get data from aredn node
100
-failedUpdateCount = 0
101
-# detected status of aredn node device
102
-nodeOnline = True
103
-
104
-# ip address of aredn node
105
-arednNodeUrl = _DEFAULT_AREDN_NODE_URL
106
-# frequency of data requests to aredn node
107
-dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
108
-# chart update interval
109
-chartUpdateInterval = _CHART_UPDATE_INTERVAL
110
-# last node request time
111
-lastDataPointTime = -1
112
-
113
-  ###  PRIVATE METHODS  ###
114
-
115
-def getTimeStamp():
116
-    """
117
-    Set the error message time stamp to the local system time.
118
-    Parameters: none
119
-    Returns: string containing the time stamp
120
-    """
121
-    return time.strftime( "%m/%d/%Y %T", time.localtime() )
122
-##end def
123
-
124
-def getEpochSeconds(sTime):
125
-    """Convert the time stamp supplied in the weather data string
126
-       to seconds since 1/1/1970 00:00:00.
127
-       Parameters: 
128
-           sTime - the time stamp to be converted must be formatted
129
-                   as %m/%d/%Y %H:%M:%S
130
-       Returns: epoch seconds
131
-    """
132
-    try:
133
-        t_sTime = time.strptime(sTime, '%m/%d/%Y %H:%M:%S')
134
-    except Exception, exError:
135
-        print '%s getEpochSeconds: %s' % (getTimeStamp(), exError)
136
-        return None
137
-    tSeconds = int(time.mktime(t_sTime))
138
-    return tSeconds
139
-##end def
140
-
141
-def setStatusToOffline():
142
-    """Set the detected status of the aredn node to
143
-       "offline" and inform downstream clients by removing input
144
-       and output data files.
145
-       Parameters: none
146
-       Returns: nothing
147
-    """
148
-    global nodeOnline
149
-
150
-    # Inform downstream clients by removing output data file.
151
-    if os.path.exists(_OUTPUT_DATA_FILE):
152
-       os.remove(_OUTPUT_DATA_FILE)
153
-    if os.path.exists(_DUMMY_OUTPUT_FILE):
154
-       os.remove(_DUMMY_OUTPUT_FILE)
155
-    # If the aredn node was previously online, then send
156
-    # a message that we are now offline.
157
-    if nodeOnline:
158
-        print '%s aredn node offline' % getTimeStamp()
159
-    nodeOnline = False
160
-##end def
161
-
162
-def terminateAgentProcess(signal, frame):
163
-    """Send a message to log when the agent process gets killed
164
-       by the operating system.  Inform downstream clients
165
-       by removing input and output data files.
166
-       Parameters:
167
-           signal, frame - dummy parameters
168
-       Returns: nothing
169
-    """
170
-    # Inform downstream clients by removing output data file.
171
-    if os.path.exists(_OUTPUT_DATA_FILE):
172
-       os.remove(_OUTPUT_DATA_FILE)
173
-    if os.path.exists(_DUMMY_OUTPUT_FILE):
174
-       os.remove(_DUMMY_OUTPUT_FILE)
175
-    print '%s terminating arednsig agent process' % \
176
-              (getTimeStamp())
177
-    sys.exit(0)
178
-##end def
179
-
180
-  ###  PUBLIC METHODS  ###
181
-
182
-def getArednNodeData():
183
-    """Send http request to aredn node.  The response from the
184
-       node contains the node signal data as unformatted ascii text.
185
-       Parameters: none
186
-       Returns: a string containing the node signal data if successful,
187
-                or None if not successful
188
-    """
189
-    try:
190
-        conn = urllib2.urlopen(arednNodeUrl, timeout=_HTTP_REQUEST_TIMEOUT)
191
-
192
-        # Format received data into a single string.
193
-        content = ""
194
-        for line in conn:
195
-            content += line.strip()
196
-        del conn
197
-
198
-    except Exception, exError:
199
-        # If no response is received from the device, then assume that
200
-        # the device is down or unavailable over the network.  In
201
-        # that case return None to the calling function.
202
-        print "%s http error: %s" % (getTimeStamp(), exError)
203
-        return None
204
-
205
-    if verboseDebug:
206
-        print "http request successful: %d bytes" % len(content)
207
-
208
-    return content
209
-##end def
210
-
211
-def parseNodeData(sData, dData):
212
-    """Parse the node  signal data JSON string from the aredn node
213
-       into its component parts.  
214
-       Parameters:
215
-           sData - the string containing the data to be parsed
216
-           dData - a dictionary object to contain the parsed data items
217
-       Returns: True if successful, False otherwise
218
-    """
219
-    
220
-
221
-    try:
222
-        strBeginSearch = '<nobr>Signal/Noise/Ratio</nobr></th>' \
223
-                         '<td valign=middle><nobr><big><b>'
224
-        strEndSearch = 'dB'
225
-
226
-        iBeginIndex = sData.find(strBeginSearch) + len(strBeginSearch)
227
-        iEndIndex = sData.find(strEndSearch, iBeginIndex)
228
-
229
-        if iBeginIndex == -1 or iEndIndex == -1:
230
-            raise Exception("signal data not found in status page")
231
-
232
-        snr = sData[iBeginIndex:iEndIndex]
233
-        snr = snr.replace(' ','')
234
-        lsnr = snr.split('/')
235
-
236
-        dData['time'] = getEpochSeconds(getTimeStamp())
237
-
238
-        dData['signal'] = lsnr[0]
239
-        dData['noise'] = lsnr[1]
240
-        dData['snr'] = lsnr[2]
241
-    
242
-    except Exception, exError:
243
-        print "%s parse failed: %s" % (getTimeStamp(), exError)
244
-        return False
245
-
246
-    if verboseDebug:
247
-        print "parse successful"
248
-    return True
249
-##end def
250
-
251
-def updateDatabase(dData):
252
-    """
253
-    Update the rrdtool database by executing an rrdtool system command.
254
-    Format the command using the data extracted from the aredn node
255
-    response.   
256
-    Parameters: dData - dictionary object containing data items to be
257
-                        written to the rr database file
258
-    Returns: True if successful, False otherwise
259
-    """
260
-    # Format the rrdtool update command.
261
-    strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s:%s:%s"
262
-    strCmd = strFmt % (_RRD_FILE, dData['time'], dData['signal'], \
263
-             dData['noise'], dData['snr'], '0', \
264
-             '0', '0', '0')
265
-
266
-    if verboseDebug:
267
-        print "%s" % strCmd # DEBUG
268
-
269
-    # Run the command as a subprocess.
270
-    try:
271
-        subprocess.check_output(strCmd, shell=True,  \
272
-                             stderr=subprocess.STDOUT)
273
-    except subprocess.CalledProcessError, exError:
274
-        print "%s: rrdtool update failed: %s" % \
275
-                    (getTimeStamp(), exError.output)
276
-        return False
277
-
278
-    return True
279
-##end def
280
-
281
-def writeOutputDataFile(sData, dData):
282
-    """Write node data items to the output data file, formatted as 
283
-       a Javascript file.  This file may then be accessed and used by
284
-       by downstream clients, for instance, in HTML documents.
285
-       Parameters:
286
-           sData - a string object containing the data to be written
287
-                   to the output data file
288
-       Returns: True if successful, False otherwise
289
-    """
290
-    # Write file for use by html clients.  The following two
291
-    # data items are sent to the client file.
292
-    #    * The last database update date and time
293
-    #    * The data request interval
294
-    lastUpdate = time.strftime( "%m.%d.%Y %T", 
295
-                                time.localtime(dData['time']) )
296
-    sDate = "[{\"date\":\"%s\",\"period\":\"%s\"}]" % \
297
-           (lastUpdate, chartUpdateInterval)
298
-    try:
299
-        fc = open(_DUMMY_OUTPUT_FILE, "w")
300
-        fc.write(sDate)
301
-        fc.close()
302
-    except Exception, exError:
303
-        print "%s write node file failed: %s" % (getTimeStamp(), exError)
304
-        return False
305
-
306
-    if _RELAY_SERVER:
307
-        # Write the entire node data response to the output data file.
308
-        try:
309
-            fc = open(_OUTPUT_DATA_FILE, "w")
310
-            fc.write(sData)
311
-            fc.close()
312
-        except Exception, exError:
313
-            print "%s write output file failed: %s" % \
314
-                  (getTimeStamp(), exError)
315
-            return False
316
-        if verboseDebug:
317
-            print "write output data file: %d bytes" % len(sData)
318
-
319
-    return True
320
-## end def
321
-
322
-def setNodeStatus(updateSuccess):
323
-    """Detect if aredn node is offline or not available on
324
-       the network. After a set number of attempts to get data
325
-       from the node set a flag that the node is offline.
326
-       Parameters:
327
-           updateSuccess - a boolean that is True if data request
328
-                           successful, False otherwise
329
-       Returns: nothing
330
-    """
331
-    global failedUpdateCount, nodeOnline
332
-
333
-    if updateSuccess:
334
-        failedUpdateCount = 0
335
-        # Set status and send a message to the log if the node was
336
-        # previously offline and is now online.
337
-        if not nodeOnline:
338
-            print '%s aredn node online' % getTimeStamp()
339
-            nodeOnline = True
340
-    else:
341
-        # The last attempt failed, so update the failed attempts
342
-        # count.
343
-        failedUpdateCount += 1
344
-
345
-    if failedUpdateCount > _MAX_FAILED_DATA_REQUESTS:
346
-        # Max number of failed data requests, so set
347
-        # node status to offline.
348
-        setStatusToOffline()
349
-##end def
350
-
351
-def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
352
-                lower, upper, addTrend, autoScale):
353
-    """Uses rrdtool to create a graph of specified node data item.
354
-       Parameters:
355
-           fileName - name of file containing the graph
356
-           dataItem - data item to be graphed
357
-           gLabel - string containing a graph label for the data item
358
-           gTitle - string containing a title for the graph
359
-           gStart - beginning time of the graphed data
360
-           lower - lower bound for graph ordinate #NOT USED
361
-           upper - upper bound for graph ordinate #NOT USED
362
-           addTrend - 0, show only graph data
363
-                      1, show only a trend line
364
-                      2, show a trend line and the graph data
365
-           autoScale - if True, then use vertical axis auto scaling
366
-               (lower and upper parameters are ignored), otherwise use
367
-               lower and upper parameters to set vertical axis scale
368
-       Returns: True if successful, False otherwise
369
-    """
370
-    gPath = _CHARTS_DIRECTORY + fileName + ".png"
371
-    trendWindow = { 'end-1day': 7200,
372
-                    'end-4weeks': 172800,
373
-                    'end-12months': 604800 }
374
- 
375
-    # Format the rrdtool graph command.
376
-
377
-    # Set chart start time, height, and width.
378
-    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
379
-             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
380
-   
381
-    # Set the range and scaling of the chart y-axis.
382
-    if lower < upper:
383
-        strCmd  +=  "-l %s -u %s -r " % (lower, upper)
384
-    elif autoScale:
385
-        strCmd += "-A "
386
-    strCmd += "-Y "
387
-
388
-    # Set the chart ordinate label and chart title. 
389
-    strCmd += "-v %s -t %s " % (gLabel, gTitle)
390
- 
391
-    # Show the data, or a moving average trend line over
392
-    # the data, or both.
393
-    strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem)
394
-    if addTrend == 0:
395
-        strCmd += "LINE1:dSeries#0400ff "
396
-    elif addTrend == 1:
397
-        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
398
-                  % trendWindow[gStart]
399
-    elif addTrend == 2:
400
-        strCmd += "LINE1:dSeries#0400ff "
401
-        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
402
-                  % trendWindow[gStart]
403
-     
404
-    if verboseDebug:
405
-        print "%s" % strCmd # DEBUG
406
-    
407
-    # Run the formatted rrdtool command as a subprocess.
408
-    try:
409
-        result = subprocess.check_output(strCmd, \
410
-                     stderr=subprocess.STDOUT,   \
411
-                     shell=True)
412
-    except subprocess.CalledProcessError, exError:
413
-        print "rrdtool graph failed: %s" % (exError.output)
414
-        return False
415
-
416
-    if debugOption:
417
-        print "rrdtool graph: %s\n" % result,
418
-    return True
419
-
420
-##end def
421
-
422
-def generateGraphs():
423
-    """Generate graphs for display in html documents.
424
-       Parameters: none
425
-       Returns: nothing
426
-    """
427
-    autoScale = False
428
-
429
-    # The following will force creation of charts
430
-    # of only signal strength and S/N charts.  Note that the following
431
-    # data items appear constant and do not show variation with time:
432
-    # noise level, rx mcs, rx rate, tx mcs, tx rate.  Therefore, until
433
-    # these parameters are demonstrated to vary in time, there is no point
434
-    # in creating the charts for these data items.
435
-    createAllCharts = False
436
-
437
-    # 24 hour stock charts
438
-
439
-    createGraph('24hr_signal', 'S', 'dBm', 
440
-                'RSSI\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
441
-    createGraph('24hr_snr', 'SNR', 'dB', 
442
-                'SNR\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
443
-
444
-    # 4 week stock charts
445
-
446
-    createGraph('4wk_signal', 'S', 'dBm', 
447
-                'RSSI\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
448
-    createGraph('4wk_snr', 'SNR', 'dB', 
449
-                'SNR\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
450
-
451
-    # 12 month stock charts
452
-
453
-    createGraph('12m_signal', 'S', 'dBm', 
454
-                'RSSI\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
455
-    createGraph('12m_snr', 'SNR', 'dB', 
456
-                'SNR\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
457
-
458
-    if debugOption:
459
-        #print # print a blank line to improve readability when in debug mode
460
-        pass
461
-##end def
462
-
463
-def getCLarguments():
464
-    """Get command line arguments.  There are four possible arguments
465
-          -d turns on debug mode
466
-          -v turns on verbose debug mode
467
-          -t sets the aredn node query interval
468
-          -u sets the url of the aredn nodeing device
469
-       Returns: nothing
470
-    """
471
-    global debugOption, verboseDebug, dataRequestInterval, \
472
-           arednNodeUrl
473
-
474
-    index = 1
475
-    while index < len(sys.argv):
476
-        if sys.argv[index] == '-d':
477
-            debugOption = True
478
-        elif sys.argv[index] == '-v':
479
-            debugOption = True
480
-            verboseDebug = True
481
-        elif sys.argv[index] == '-p':
482
-            try:
483
-                dataRequestInterval = abs(int(sys.argv[index + 1]))
484
-            except:
485
-                print "invalid polling period"
486
-                exit(-1)
487
-            index += 1
488
-        elif sys.argv[index] == '-u':
489
-            arednNodeUrl = sys.argv[index + 1]
490
-            index += 1
491
-        else:
492
-            cmd_name = sys.argv[0].split('/')
493
-            print "Usage: %s [-d] [-v] [-p seconds] [-u url]" % cmd_name[-1]
494
-            exit(-1)
495
-        index += 1
496
-##end def
497
-
498
-def main():
499
-    """Handles timing of events and acts as executive routine managing
500
-       all other functions.
501
-       Parameters: none
502
-       Returns: nothing
503
-    """
504
-    global dataRequestInterval
505
-
506
-    signal.signal(signal.SIGTERM, terminateAgentProcess)
507
-
508
-    print '%s starting up arednsig agent process' % \
509
-                  (getTimeStamp())
510
-
511
-    # last time output JSON file updated
512
-    lastDataRequestTime = -1
513
-    # last time charts generated
514
-    lastChartUpdateTime = - 1
515
-    # last time the rrdtool database updated
516
-    lastDatabaseUpdateTime = -1
517
-
518
-    ## Get command line arguments.
519
-    getCLarguments()
520
-
521
-    ## Exit with error if rrdtool database does not exist.
522
-    if not os.path.exists(_RRD_FILE):
523
-        print 'rrdtool database does not exist\n' \
524
-              'use createArednsigRrd script to ' \
525
-              'create rrdtool database\n'
526
-        exit(1)
527
- 
528
-    ## main loop
529
-    while True:
530
-
531
-        currentTime = time.time() # get current time in seconds
532
-
533
-        # Every web update interval request data from the aredn
534
-        # node and process the received data.
535
-        if currentTime - lastDataRequestTime > dataRequestInterval:
536
-            lastDataRequestTime = currentTime
537
-            dData = {}
538
-            result = True
539
-
540
-            # Get the data string from the device.
541
-            sData = getArednNodeData()
542
-            # If the first http request fails, try one more time.
543
-            if sData == None:
544
-                result = False
545
-
546
-            # If successful parse the data.
547
-            if result:
548
-                result = parseNodeData(sData, dData)
549
-           
550
-            # If parse successful, write data to data files.
551
-            if result:
552
-                result = updateDatabase(dData)
553
-
554
-            if result:
555
-                writeOutputDataFile(sData, dData)
556
-
557
-            # Set the node status to online or offline depending on the
558
-            # success or failure of the above operations.
559
-            setNodeStatus(result)
560
-
561
-
562
-        # At the chart generation interval, generate charts.
563
-        if currentTime - lastChartUpdateTime > chartUpdateInterval:
564
-            lastChartUpdateTime = currentTime
565
-            p = multiprocessing.Process(target=generateGraphs, args=())
566
-            p.start()
567
-
568
-        # Relinquish processing back to the operating system until
569
-        # the next update interval.
570
-
571
-        elapsedTime = time.time() - currentTime
572
-        if debugOption:
573
-            if result:
574
-                print "%s update successful:" % getTimeStamp(),
575
-            else:
576
-                print "%s update failed:" % getTimeStamp(),
577
-            print "%6f seconds processing time\n" % elapsedTime 
578
-        remainingTime = dataRequestInterval - elapsedTime
579
-        if remainingTime > 0.0:
580
-            time.sleep(remainingTime)
581
-    ## end while
582
-    return
583
-## end def
584
-
585
-if __name__ == '__main__':
586
-    try:
587
-        main()
588
-    except KeyboardInterrupt:
589
-        print '\n',
590
-        terminateAgentProcess('KeyboardInterrupt','Module')
Browse code

add version for FW v3.20

Gandolf authored on 10/03/2020 23:02:19
Showing 1 changed files
1 1
new file mode 100755
... ...
@@ -0,0 +1,590 @@
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: arednsigAgent.py
6
+#
7
+# Description: This module acts as an agent between the aredn node
8
+# and aredn mest services.  The agent periodically sends an http
9
+# request to the aredn node, processes the response from
10
+# the node, and performs a number of operations:
11
+#     - conversion of data items
12
+#     - update a round robin (rrdtool) database with the node data
13
+#     - periodically generate graphic charts for display in html documents
14
+#     - write the processed node status to a JSON file for use by html
15
+#       documents
16
+#
17
+# Copyright 2020 Jeff Owrey
18
+#    This program is free software: you can redistribute it and/or modify
19
+#    it under the terms of the GNU General Public License as published by
20
+#    the Free Software Foundation, either version 3 of the License, or
21
+#    (at your option) any later version.
22
+#
23
+#    This program is distributed in the hope that it will be useful,
24
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
25
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26
+#    GNU General Public License for more details.
27
+#
28
+#    You should have received a copy of the GNU General Public License
29
+#    along with this program.  If not, see http://www.gnu.org/license.
30
+#
31
+# Revision History
32
+#   * v20 released 11 Jan 2020 by J L Owrey; first release
33
+#   * v21 released 13 Feb 2020 by J L Owrey; fixed bug occuring when node
34
+#     powers on and signal data memory is empty.  Data points with N/A data
35
+#     are discarded.
36
+#   * v22 released 31 Mar 2020 by J L Owrey; upgraded for compatibility with
37
+#     Aredn firmware version 3.20.3.0.  This agent now downloads the node's
38
+#     status page and parsed the signal data from the html.
39
+#
40
+#2345678901234567890123456789012345678901234567890123456789012345678901234567890
41
+
42
+import os
43
+import urllib2
44
+import sys
45
+import signal
46
+import subprocess
47
+import multiprocessing
48
+import time
49
+
50
+_USER = os.environ['USER']
51
+
52
+   ### DEFAULT AREDN NODE URL ###
53
+
54
+# set url of the aredn node
55
+
56
+_DEFAULT_AREDN_NODE_URL = "http://localnode:8080/cgi-bin/status"
57
+
58
+    ### FILE AND FOLDER LOCATIONS ###
59
+
60
+# folder for containing dynamic data objects
61
+_DOCROOT_PATH = "/home/%s/public_html/arednsig/" % _USER
62
+# folder for charts and output data file
63
+_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/"
64
+# location of data output file
65
+_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/arednsigOutputData.js"
66
+# dummy output data file
67
+_DUMMY_OUTPUT_FILE = _DOCROOT_PATH + "dynamic/nodeOnline.js"
68
+# database that stores node data
69
+_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
70
+
71
+    ### GLOBAL CONSTANTS ###
72
+
73
+# interval in seconds between data requests to the aredn node
74
+_DEFAULT_DATA_REQUEST_INTERVAL = 60
75
+# chart update interval in minutes
76
+_CHART_UPDATE_INTERVAL = 600
77
+
78
+# number seconds to wait for a response to HTTP request
79
+_HTTP_REQUEST_TIMEOUT = 10
80
+# max number of failed data requests allowed
81
+_MAX_FAILED_DATA_REQUESTS = 0
82
+# standard chart width in pixels
83
+_CHART_WIDTH = 600
84
+# standard chart height in pixels
85
+_CHART_HEIGHT = 150
86
+# Set this to True only if this server is intended to relay raw
87
+# node data to a mirror server.
88
+_RELAY_SERVER = False
89
+
90
+   ### GLOBAL VARIABLES ###
91
+
92
+# turn on or off of verbose debugging information
93
+debugOption = False
94
+verboseDebug = False
95
+
96
+# The following two items are used for detecting system faults
97
+# and aredn node online or offline status.
98
+
99
+# count of failed attempts to get data from aredn node
100
+failedUpdateCount = 0
101
+# detected status of aredn node device
102
+nodeOnline = True
103
+
104
+# ip address of aredn node
105
+arednNodeUrl = _DEFAULT_AREDN_NODE_URL
106
+# frequency of data requests to aredn node
107
+dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
108
+# chart update interval
109
+chartUpdateInterval = _CHART_UPDATE_INTERVAL
110
+# last node request time
111
+lastDataPointTime = -1
112
+
113
+  ###  PRIVATE METHODS  ###
114
+
115
+def getTimeStamp():
116
+    """
117
+    Set the error message time stamp to the local system time.
118
+    Parameters: none
119
+    Returns: string containing the time stamp
120
+    """
121
+    return time.strftime( "%m/%d/%Y %T", time.localtime() )
122
+##end def
123
+
124
+def getEpochSeconds(sTime):
125
+    """Convert the time stamp supplied in the weather data string
126
+       to seconds since 1/1/1970 00:00:00.
127
+       Parameters: 
128
+           sTime - the time stamp to be converted must be formatted
129
+                   as %m/%d/%Y %H:%M:%S
130
+       Returns: epoch seconds
131
+    """
132
+    try:
133
+        t_sTime = time.strptime(sTime, '%m/%d/%Y %H:%M:%S')
134
+    except Exception, exError:
135
+        print '%s getEpochSeconds: %s' % (getTimeStamp(), exError)
136
+        return None
137
+    tSeconds = int(time.mktime(t_sTime))
138
+    return tSeconds
139
+##end def
140
+
141
+def setStatusToOffline():
142
+    """Set the detected status of the aredn node to
143
+       "offline" and inform downstream clients by removing input
144
+       and output data files.
145
+       Parameters: none
146
+       Returns: nothing
147
+    """
148
+    global nodeOnline
149
+
150
+    # Inform downstream clients by removing output data file.
151
+    if os.path.exists(_OUTPUT_DATA_FILE):
152
+       os.remove(_OUTPUT_DATA_FILE)
153
+    if os.path.exists(_DUMMY_OUTPUT_FILE):
154
+       os.remove(_DUMMY_OUTPUT_FILE)
155
+    # If the aredn node was previously online, then send
156
+    # a message that we are now offline.
157
+    if nodeOnline:
158
+        print '%s aredn node offline' % getTimeStamp()
159
+    nodeOnline = False
160
+##end def
161
+
162
+def terminateAgentProcess(signal, frame):
163
+    """Send a message to log when the agent process gets killed
164
+       by the operating system.  Inform downstream clients
165
+       by removing input and output data files.
166
+       Parameters:
167
+           signal, frame - dummy parameters
168
+       Returns: nothing
169
+    """
170
+    # Inform downstream clients by removing output data file.
171
+    if os.path.exists(_OUTPUT_DATA_FILE):
172
+       os.remove(_OUTPUT_DATA_FILE)
173
+    if os.path.exists(_DUMMY_OUTPUT_FILE):
174
+       os.remove(_DUMMY_OUTPUT_FILE)
175
+    print '%s terminating arednsig agent process' % \
176
+              (getTimeStamp())
177
+    sys.exit(0)
178
+##end def
179
+
180
+  ###  PUBLIC METHODS  ###
181
+
182
+def getArednNodeData():
183
+    """Send http request to aredn node.  The response from the
184
+       node contains the node signal data as unformatted ascii text.
185
+       Parameters: none
186
+       Returns: a string containing the node signal data if successful,
187
+                or None if not successful
188
+    """
189
+    try:
190
+        conn = urllib2.urlopen(arednNodeUrl, timeout=_HTTP_REQUEST_TIMEOUT)
191
+
192
+        # Format received data into a single string.
193
+        content = ""
194
+        for line in conn:
195
+            content += line.strip()
196
+        del conn
197
+
198
+    except Exception, exError:
199
+        # If no response is received from the device, then assume that
200
+        # the device is down or unavailable over the network.  In
201
+        # that case return None to the calling function.
202
+        print "%s http error: %s" % (getTimeStamp(), exError)
203
+        return None
204
+
205
+    if verboseDebug:
206
+        print "http request successful: %d bytes" % len(content)
207
+
208
+    return content
209
+##end def
210
+
211
+def parseNodeData(sData, dData):
212
+    """Parse the node  signal data JSON string from the aredn node
213
+       into its component parts.  
214
+       Parameters:
215
+           sData - the string containing the data to be parsed
216
+           dData - a dictionary object to contain the parsed data items
217
+       Returns: True if successful, False otherwise
218
+    """
219
+    
220
+
221
+    try:
222
+        strBeginSearch = '<nobr>Signal/Noise/Ratio</nobr></th>' \
223
+                         '<td valign=middle><nobr><big><b>'
224
+        strEndSearch = 'dB'
225
+
226
+        iBeginIndex = sData.find(strBeginSearch) + len(strBeginSearch)
227
+        iEndIndex = sData.find(strEndSearch, iBeginIndex)
228
+
229
+        if iBeginIndex == -1 or iEndIndex == -1:
230
+            raise Exception("signal data not found in status page")
231
+
232
+        snr = sData[iBeginIndex:iEndIndex]
233
+        snr = snr.replace(' ','')
234
+        lsnr = snr.split('/')
235
+
236
+        dData['time'] = getEpochSeconds(getTimeStamp())
237
+
238
+        dData['signal'] = lsnr[0]
239
+        dData['noise'] = lsnr[1]
240
+        dData['snr'] = lsnr[2]
241
+    
242
+    except Exception, exError:
243
+        print "%s parse failed: %s" % (getTimeStamp(), exError)
244
+        return False
245
+
246
+    if verboseDebug:
247
+        print "parse successful"
248
+    return True
249
+##end def
250
+
251
+def updateDatabase(dData):
252
+    """
253
+    Update the rrdtool database by executing an rrdtool system command.
254
+    Format the command using the data extracted from the aredn node
255
+    response.   
256
+    Parameters: dData - dictionary object containing data items to be
257
+                        written to the rr database file
258
+    Returns: True if successful, False otherwise
259
+    """
260
+    # Format the rrdtool update command.
261
+    strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s:%s:%s"
262
+    strCmd = strFmt % (_RRD_FILE, dData['time'], dData['signal'], \
263
+             dData['noise'], dData['snr'], '0', \
264
+             '0', '0', '0')
265
+
266
+    if verboseDebug:
267
+        print "%s" % strCmd # DEBUG
268
+
269
+    # Run the command as a subprocess.
270
+    try:
271
+        subprocess.check_output(strCmd, shell=True,  \
272
+                             stderr=subprocess.STDOUT)
273
+    except subprocess.CalledProcessError, exError:
274
+        print "%s: rrdtool update failed: %s" % \
275
+                    (getTimeStamp(), exError.output)
276
+        return False
277
+
278
+    return True
279
+##end def
280
+
281
+def writeOutputDataFile(sData, dData):
282
+    """Write node data items to the output data file, formatted as 
283
+       a Javascript file.  This file may then be accessed and used by
284
+       by downstream clients, for instance, in HTML documents.
285
+       Parameters:
286
+           sData - a string object containing the data to be written
287
+                   to the output data file
288
+       Returns: True if successful, False otherwise
289
+    """
290
+    # Write file for use by html clients.  The following two
291
+    # data items are sent to the client file.
292
+    #    * The last database update date and time
293
+    #    * The data request interval
294
+    lastUpdate = time.strftime( "%m.%d.%Y %T", 
295
+                                time.localtime(dData['time']) )
296
+    sDate = "[{\"date\":\"%s\",\"period\":\"%s\"}]" % \
297
+           (lastUpdate, chartUpdateInterval)
298
+    try:
299
+        fc = open(_DUMMY_OUTPUT_FILE, "w")
300
+        fc.write(sDate)
301
+        fc.close()
302
+    except Exception, exError:
303
+        print "%s write node file failed: %s" % (getTimeStamp(), exError)
304
+        return False
305
+
306
+    if _RELAY_SERVER:
307
+        # Write the entire node data response to the output data file.
308
+        try:
309
+            fc = open(_OUTPUT_DATA_FILE, "w")
310
+            fc.write(sData)
311
+            fc.close()
312
+        except Exception, exError:
313
+            print "%s write output file failed: %s" % \
314
+                  (getTimeStamp(), exError)
315
+            return False
316
+        if verboseDebug:
317
+            print "write output data file: %d bytes" % len(sData)
318
+
319
+    return True
320
+## end def
321
+
322
+def setNodeStatus(updateSuccess):
323
+    """Detect if aredn node is offline or not available on
324
+       the network. After a set number of attempts to get data
325
+       from the node set a flag that the node is offline.
326
+       Parameters:
327
+           updateSuccess - a boolean that is True if data request
328
+                           successful, False otherwise
329
+       Returns: nothing
330
+    """
331
+    global failedUpdateCount, nodeOnline
332
+
333
+    if updateSuccess:
334
+        failedUpdateCount = 0
335
+        # Set status and send a message to the log if the node was
336
+        # previously offline and is now online.
337
+        if not nodeOnline:
338
+            print '%s aredn node online' % getTimeStamp()
339
+            nodeOnline = True
340
+    else:
341
+        # The last attempt failed, so update the failed attempts
342
+        # count.
343
+        failedUpdateCount += 1
344
+
345
+    if failedUpdateCount > _MAX_FAILED_DATA_REQUESTS:
346
+        # Max number of failed data requests, so set
347
+        # node status to offline.
348
+        setStatusToOffline()
349
+##end def
350
+
351
+def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
352
+                lower, upper, addTrend, autoScale):
353
+    """Uses rrdtool to create a graph of specified node data item.
354
+       Parameters:
355
+           fileName - name of file containing the graph
356
+           dataItem - data item to be graphed
357
+           gLabel - string containing a graph label for the data item
358
+           gTitle - string containing a title for the graph
359
+           gStart - beginning time of the graphed data
360
+           lower - lower bound for graph ordinate #NOT USED
361
+           upper - upper bound for graph ordinate #NOT USED
362
+           addTrend - 0, show only graph data
363
+                      1, show only a trend line
364
+                      2, show a trend line and the graph data
365
+           autoScale - if True, then use vertical axis auto scaling
366
+               (lower and upper parameters are ignored), otherwise use
367
+               lower and upper parameters to set vertical axis scale
368
+       Returns: True if successful, False otherwise
369
+    """
370
+    gPath = _CHARTS_DIRECTORY + fileName + ".png"
371
+    trendWindow = { 'end-1day': 7200,
372
+                    'end-4weeks': 172800,
373
+                    'end-12months': 604800 }
374
+ 
375
+    # Format the rrdtool graph command.
376
+
377
+    # Set chart start time, height, and width.
378
+    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
379
+             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
380
+   
381
+    # Set the range and scaling of the chart y-axis.
382
+    if lower < upper:
383
+        strCmd  +=  "-l %s -u %s -r " % (lower, upper)
384
+    elif autoScale:
385
+        strCmd += "-A "
386
+    strCmd += "-Y "
387
+
388
+    # Set the chart ordinate label and chart title. 
389
+    strCmd += "-v %s -t %s " % (gLabel, gTitle)
390
+ 
391
+    # Show the data, or a moving average trend line over
392
+    # the data, or both.
393
+    strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem)
394
+    if addTrend == 0:
395
+        strCmd += "LINE1:dSeries#0400ff "
396
+    elif addTrend == 1:
397
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
398
+                  % trendWindow[gStart]
399
+    elif addTrend == 2:
400
+        strCmd += "LINE1:dSeries#0400ff "
401
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
402
+                  % trendWindow[gStart]
403
+     
404
+    if verboseDebug:
405
+        print "%s" % strCmd # DEBUG
406
+    
407
+    # Run the formatted rrdtool command as a subprocess.
408
+    try:
409
+        result = subprocess.check_output(strCmd, \
410
+                     stderr=subprocess.STDOUT,   \
411
+                     shell=True)
412
+    except subprocess.CalledProcessError, exError:
413
+        print "rrdtool graph failed: %s" % (exError.output)
414
+        return False
415
+
416
+    if debugOption:
417
+        print "rrdtool graph: %s\n" % result,
418
+    return True
419
+
420
+##end def
421
+
422
+def generateGraphs():
423
+    """Generate graphs for display in html documents.
424
+       Parameters: none
425
+       Returns: nothing
426
+    """
427
+    autoScale = False
428
+
429
+    # The following will force creation of charts
430
+    # of only signal strength and S/N charts.  Note that the following
431
+    # data items appear constant and do not show variation with time:
432
+    # noise level, rx mcs, rx rate, tx mcs, tx rate.  Therefore, until
433
+    # these parameters are demonstrated to vary in time, there is no point
434
+    # in creating the charts for these data items.
435
+    createAllCharts = False
436
+
437
+    # 24 hour stock charts
438
+
439
+    createGraph('24hr_signal', 'S', 'dBm', 
440
+                'RSSI\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
441
+    createGraph('24hr_snr', 'SNR', 'dB', 
442
+                'SNR\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
443
+
444
+    # 4 week stock charts
445
+
446
+    createGraph('4wk_signal', 'S', 'dBm', 
447
+                'RSSI\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
448
+    createGraph('4wk_snr', 'SNR', 'dB', 
449
+                'SNR\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
450
+
451
+    # 12 month stock charts
452
+
453
+    createGraph('12m_signal', 'S', 'dBm', 
454
+                'RSSI\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
455
+    createGraph('12m_snr', 'SNR', 'dB', 
456
+                'SNR\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
457
+
458
+    if debugOption:
459
+        #print # print a blank line to improve readability when in debug mode
460
+        pass
461
+##end def
462
+
463
+def getCLarguments():
464
+    """Get command line arguments.  There are four possible arguments
465
+          -d turns on debug mode
466
+          -v turns on verbose debug mode
467
+          -t sets the aredn node query interval
468
+          -u sets the url of the aredn nodeing device
469
+       Returns: nothing
470
+    """
471
+    global debugOption, verboseDebug, dataRequestInterval, \
472
+           arednNodeUrl
473
+
474
+    index = 1
475
+    while index < len(sys.argv):
476
+        if sys.argv[index] == '-d':
477
+            debugOption = True
478
+        elif sys.argv[index] == '-v':
479
+            debugOption = True
480
+            verboseDebug = True
481
+        elif sys.argv[index] == '-p':
482
+            try:
483
+                dataRequestInterval = abs(int(sys.argv[index + 1]))
484
+            except:
485
+                print "invalid polling period"
486
+                exit(-1)
487
+            index += 1
488
+        elif sys.argv[index] == '-u':
489
+            arednNodeUrl = sys.argv[index + 1]
490
+            index += 1
491
+        else:
492
+            cmd_name = sys.argv[0].split('/')
493
+            print "Usage: %s [-d] [-v] [-p seconds] [-u url]" % cmd_name[-1]
494
+            exit(-1)
495
+        index += 1
496
+##end def
497
+
498
+def main():
499
+    """Handles timing of events and acts as executive routine managing
500
+       all other functions.
501
+       Parameters: none
502
+       Returns: nothing
503
+    """
504
+    global dataRequestInterval
505
+
506
+    signal.signal(signal.SIGTERM, terminateAgentProcess)
507
+
508
+    print '%s starting up arednsig agent process' % \
509
+                  (getTimeStamp())
510
+
511
+    # last time output JSON file updated
512
+    lastDataRequestTime = -1
513
+    # last time charts generated
514
+    lastChartUpdateTime = - 1
515
+    # last time the rrdtool database updated
516
+    lastDatabaseUpdateTime = -1
517
+
518
+    ## Get command line arguments.
519
+    getCLarguments()
520
+
521
+    ## Exit with error if rrdtool database does not exist.
522
+    if not os.path.exists(_RRD_FILE):
523
+        print 'rrdtool database does not exist\n' \
524
+              'use createArednsigRrd script to ' \
525
+              'create rrdtool database\n'
526
+        exit(1)
527
+ 
528
+    ## main loop
529
+    while True:
530
+
531
+        currentTime = time.time() # get current time in seconds
532
+
533
+        # Every web update interval request data from the aredn
534
+        # node and process the received data.
535
+        if currentTime - lastDataRequestTime > dataRequestInterval:
536
+            lastDataRequestTime = currentTime
537
+            dData = {}
538
+            result = True
539
+
540
+            # Get the data string from the device.
541
+            sData = getArednNodeData()
542
+            # If the first http request fails, try one more time.
543
+            if sData == None:
544
+                result = False
545
+
546
+            # If successful parse the data.
547
+            if result:
548
+                result = parseNodeData(sData, dData)
549
+           
550
+            # If parse successful, write data to data files.
551
+            if result:
552
+                result = updateDatabase(dData)
553
+
554
+            if result:
555
+                writeOutputDataFile(sData, dData)
556
+
557
+            # Set the node status to online or offline depending on the
558
+            # success or failure of the above operations.
559
+            setNodeStatus(result)
560
+
561
+
562
+        # At the chart generation interval, generate charts.
563
+        if currentTime - lastChartUpdateTime > chartUpdateInterval:
564
+            lastChartUpdateTime = currentTime
565
+            p = multiprocessing.Process(target=generateGraphs, args=())
566
+            p.start()
567
+
568
+        # Relinquish processing back to the operating system until
569
+        # the next update interval.
570
+
571
+        elapsedTime = time.time() - currentTime
572
+        if debugOption:
573
+            if result:
574
+                print "%s update successful:" % getTimeStamp(),
575
+            else:
576
+                print "%s update failed:" % getTimeStamp(),
577
+            print "%6f seconds processing time\n" % elapsedTime 
578
+        remainingTime = dataRequestInterval - elapsedTime
579
+        if remainingTime > 0.0:
580
+            time.sleep(remainingTime)
581
+    ## end while
582
+    return
583
+## end def
584
+
585
+if __name__ == '__main__':
586
+    try:
587
+        main()
588
+    except KeyboardInterrupt:
589
+        print '\n',
590
+        terminateAgentProcess('KeyboardInterrupt','Module')
Browse code

remove old versions

Gandolf authored on 10/03/2020 23:00:21
Showing 1 changed files
1 1
deleted file mode 100755
... ...
@@ -1,592 +0,0 @@
1
-#!/usr/bin/python -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: arednsigAgent.py
6
-#
7
-# Description: This module acts as an agent between the aredn node
8
-# and aredn mest services.  The agent periodically sends an http
9
-# request to the aredn node, processes the response from
10
-# the node, and performs a number of operations:
11
-#     - conversion of data items
12
-#     - update a round robin (rrdtool) database with the node data
13
-#     - periodically generate graphic charts for display in html documents
14
-#     - write the processed node status to a JSON file for use by html
15
-#       documents
16
-#
17
-# Copyright 2020 Jeff Owrey
18
-#    This program is free software: you can redistribute it and/or modify
19
-#    it under the terms of the GNU General Public License as published by
20
-#    the Free Software Foundation, either version 3 of the License, or
21
-#    (at your option) any later version.
22
-#
23
-#    This program is distributed in the hope that it will be useful,
24
-#    but WITHOUT ANY WARRANTY; without even the implied warranty of
25
-#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26
-#    GNU General Public License for more details.
27
-#
28
-#    You should have received a copy of the GNU General Public License
29
-#    along with this program.  If not, see http://www.gnu.org/license.
30
-#
31
-# Revision History
32
-#   * v20 released 11 Jan 2020 by J L Owrey; first release
33
-#   * v21 released 13 Feb 2020 by J L Owrey; fixed bug occuring when node
34
-#     powers on and signal data memory is empty.  Data points with N/A data
35
-#     are discarded.
36
-#   * v22 released 31 Mar 2020 by J L Owrey; upgraded for compatibility with
37
-#     Aredn firmware version 3.20.3.0.  This agent now downloads the node's
38
-#     status page and parsed the signal data from the html.
39
-#
40
-#2345678901234567890123456789012345678901234567890123456789012345678901234567890
41
-
42
-import os
43
-import urllib2
44
-import sys
45
-import signal
46
-import subprocess
47
-import multiprocessing
48
-import time
49
-
50
-_USER = os.environ['USER']
51
-_HOSTNAME = os.uname()[1]
52
-
53
-   ### DEFAULT AREDN NODE URL ###
54
-
55
-# set url of the aredn node
56
-
57
-_DEFAULT_AREDN_NODE_URL = "http://localnode:8080/cgi-bin/status"
58
-
59
-    ### FILE AND FOLDER LOCATIONS ###
60
-
61
-# folder for containing dynamic data objects
62
-_DOCROOT_PATH = "/home/%s/public_html/arednsig/" % _USER
63
-# folder for charts and output data file
64
-_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/"
65
-# location of data output file
66
-_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/arednsigOutputData.js"
67
-# dummy output data file
68
-_DUMMY_OUTPUT_FILE = _DOCROOT_PATH + "dynamic/nodeOnline.js"
69
-# database that stores node data
70
-_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
71
-
72
-    ### GLOBAL CONSTANTS ###
73
-
74
-# interval in minutes between data requests to the aredn node
75
-_DEFAULT_DATA_REQUEST_INTERVAL = 1
76
-# chart update interval in minutes
77
-_CHART_UPDATE_INTERVAL = 10
78
-
79
-# number seconds to wait for a response to HTTP request
80
-_HTTP_REQUEST_TIMEOUT = 10
81
-# max number of failed data requests allowed
82
-_MAX_FAILED_DATA_REQUESTS = 0
83
-# standard chart width in pixels
84
-_CHART_WIDTH = 600
85
-# standard chart height in pixels
86
-_CHART_HEIGHT = 150
87
-# Set this to True only if this server is intended to relay raw
88
-# node data to a mirror server.
89
-_RELAY_SERVER = False
90
-
91
-   ### GLOBAL VARIABLES ###
92
-
93
-# turn on or off of verbose debugging information
94
-debugOption = False
95
-verboseDebug = False
96
-
97
-# The following two items are used for detecting system faults
98
-# and aredn node online or offline status.
99
-
100
-# count of failed attempts to get data from aredn node
101
-failedUpdateCount = 0
102
-# detected status of aredn node device
103
-nodeOnline = True
104
-
105
-# ip address of aredn node
106
-arednNodeUrl = _DEFAULT_AREDN_NODE_URL
107
-# frequency of data requests to aredn node
108
-dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
109
-# chart update interval
110
-chartUpdateInterval = _CHART_UPDATE_INTERVAL
111
-# last node request time
112
-lastDataPointTime = -1
113
-
114
-  ###  PRIVATE METHODS  ###
115
-
116
-def getTimeStamp():
117
-    """
118
-    Set the error message time stamp to the local system time.
119
-    Parameters: none
120
-    Returns: string containing the time stamp
121
-    """
122
-    return time.strftime( "%m/%d/%Y %T", time.localtime() )
123
-##end def
124
-
125
-def getEpochSeconds(sTime):
126
-    """Convert the time stamp supplied in the weather data string
127
-       to seconds since 1/1/1970 00:00:00.
128
-       Parameters: 
129
-           sTime - the time stamp to be converted must be formatted
130
-                   as %m/%d/%Y %H:%M:%S
131
-       Returns: epoch seconds
132
-    """
133
-    try:
134
-        t_sTime = time.strptime(sTime, '%m/%d/%Y %H:%M:%S')
135
-    except Exception, exError:
136
-        print '%s getEpochSeconds: %s' % (getTimeStamp(), exError)
137
-        return None
138
-    tSeconds = int(time.mktime(t_sTime))
139
-    return tSeconds
140
-##end def
141
-
142
-def setStatusToOffline():
143
-    """Set the detected status of the aredn node to
144
-       "offline" and inform downstream clients by removing input
145
-       and output data files.
146
-       Parameters: none
147
-       Returns: nothing
148
-    """
149
-    global nodeOnline
150
-
151
-    # Inform downstream clients by removing output data file.
152
-    if os.path.exists(_OUTPUT_DATA_FILE):
153
-       os.remove(_OUTPUT_DATA_FILE)
154
-    if os.path.exists(_DUMMY_OUTPUT_FILE):
155
-       os.remove(_DUMMY_OUTPUT_FILE)
156
-    # If the aredn node was previously online, then send
157
-    # a message that we are now offline.
158
-    if nodeOnline:
159
-        print '%s aredn node offline' % getTimeStamp()
160
-    nodeOnline = False
161
-##end def
162
-
163
-def terminateAgentProcess(signal, frame):
164
-    """Send a message to log when the agent process gets killed
165
-       by the operating system.  Inform downstream clients
166
-       by removing input and output data files.
167
-       Parameters:
168
-           signal, frame - dummy parameters
169
-       Returns: nothing
170
-    """
171
-    # Inform downstream clients by removing output data file.
172
-    if os.path.exists(_OUTPUT_DATA_FILE):
173
-       os.remove(_OUTPUT_DATA_FILE)
174
-    if os.path.exists(_DUMMY_OUTPUT_FILE):
175
-       os.remove(_DUMMY_OUTPUT_FILE)
176
-    print '%s terminating arednsig agent process' % \
177
-              (getTimeStamp())
178
-    sys.exit(0)
179
-##end def
180
-
181
-  ###  PUBLIC METHODS  ###
182
-
183
-def getArednNodeData():
184
-    """Send http request to aredn node.  The response from the
185
-       node contains the node signal data as unformatted ascii text.
186
-       Parameters: none
187
-       Returns: a string containing the node signal data if successful,
188
-                or None if not successful
189
-    """
190
-    try:
191
-        conn = urllib2.urlopen(arednNodeUrl, timeout=_HTTP_REQUEST_TIMEOUT)
192
-
193
-        # Format received data into a single string.
194
-        content = ""
195
-        for line in conn:
196
-            content += line.strip()
197
-        del conn
198
-
199
-    except Exception, exError:
200
-        # If no response is received from the device, then assume that
201
-        # the device is down or unavailable over the network.  In
202
-        # that case return None to the calling function.
203
-        print "%s http error: %s" % (getTimeStamp(), exError)
204
-        return None
205
-
206
-    if verboseDebug:
207
-        print "http request successful: %d bytes" % len(content)
208
-
209
-    return content
210
-##end def
211
-
212
-def parseNodeData(sData, dData):
213
-    """Parse the node status page html from the aredn node
214
-       into its component parts.  
215
-       Parameters:
216
-           sData - the string containing the data to be parsed
217
-           dData - a dictionary object to contain the parsed data items
218
-       Returns: True if successful, False otherwise
219
-    """
220
-    try:
221
-
222
-        # Set search boundaries for signal data
223
-        strBeginSearch = '<nobr>Signal/Noise/Ratio</nobr></th>' \
224
-                         '<td valign=middle><nobr><big><b>'
225
-        strEndSearch = 'dB'
226
-        iBeginIndex = sData.find(strBeginSearch) + len(strBeginSearch)
227
-        iEndIndex = sData.find(strEndSearch, iBeginIndex)
228
-
229
-        # Exception if signal data not found
230
-        if iBeginIndex == -1 or iEndIndex == -1:
231
-            raise Exception("signal data not found in status page")
232
-
233
-        # Extract signal data from html
234
-        snr = sData[iBeginIndex:iEndIndex]
235
-        snr = snr.replace(' ','')
236
-        lsnr = snr.split('/')
237
-
238
-        # Store time and signal data in dictionary object
239
-        dData['time'] = getEpochSeconds(getTimeStamp())
240
-        dData['signal'] = lsnr[0]
241
-        dData['noise'] = lsnr[1]
242
-        dData['snr'] = lsnr[2]
243
-    
244
-    except Exception, exError:
245
-        print "%s parse failed: %s" % (getTimeStamp(), exError)
246
-        return False
247
-
248
-    if verboseDebug:
249
-        print "parse successful"
250
-    return True
251
-##end def
252
-
253
-def updateDatabase(dData):
254
-    """
255
-    Update the rrdtool database by executing an rrdtool system command.
256
-    Format the command using the data extracted from the aredn node
257
-    response.   
258
-    Parameters: dData - dictionary object containing data items to be
259
-                        written to the rr database file
260
-    Returns: True if successful, False otherwise
261
-    """
262
-    # Format the rrdtool update command.
263
-    strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s:%s:%s"
264
-    strCmd = strFmt % (_RRD_FILE, dData['time'], dData['signal'], \
265
-             dData['noise'], dData['snr'], '0', \
266
-             '0', '0', '0')
267
-
268
-    if verboseDebug:
269
-        print "%s" % strCmd # DEBUG
270
-
271
-    # Run the command as a subprocess.
272
-    try:
273
-        subprocess.check_output(strCmd, shell=True,  \
274
-                             stderr=subprocess.STDOUT)
275
-    except subprocess.CalledProcessError, exError:
276
-        print "%s: rrdtool update failed: %s" % \
277
-                    (getTimeStamp(), exError.output)
278
-        return False
279
-
280
-    return True
281
-##end def
282
-
283
-def writeOutputDataFile(sData, dData):
284
-    """Write node data items to the output data file, formatted as 
285
-       a Javascript file.  This file may then be accessed and used by
286
-       by downstream clients, for instance, in HTML documents.
287
-       Parameters:
288
-           sData - a string object containing the data to be written
289
-                   to the output data file
290
-       Returns: True if successful, False otherwise
291
-    """
292
-    # Write file for use by html clients.  The following two
293
-    # data items are sent to the client file.
294
-    #    * The last database update date and time
295
-    #    * The data request interval
296
-    lastUpdate = time.strftime( "%m.%d.%Y %T", 
297
-                                time.localtime(dData['time']) )
298
-    sDate = "[{\"date\":\"%s\",\"period\":\"%s\"}]" % \
299
-           (lastUpdate, chartUpdateInterval)
300
-    try:
301
-        fc = open(_DUMMY_OUTPUT_FILE, "w")
302
-        fc.write(sDate)
303
-        fc.close()
304
-    except Exception, exError:
305
-        print "%s write node file failed: %s" % (getTimeStamp(), exError)
306
-        return False
307
-
308
-    if _RELAY_SERVER:
309
-        # Write the entire node data response to the output data file.
310
-        try:
311
-            fc = open(_OUTPUT_DATA_FILE, "w")
312
-            fc.write(sData)
313
-            fc.close()
314
-        except Exception, exError:
315
-            print "%s write output file failed: %s" % \
316
-                  (getTimeStamp(), exError)
317
-            return False
318
-        if verboseDebug:
319
-            print "write output data file: %d bytes" % len(sData)
320
-
321
-    return True
322
-## end def
323
-
324
-def setNodeStatus(updateSuccess):
325
-    """Detect if aredn node is offline or not available on
326
-       the network. After a set number of attempts to get data
327
-       from the node set a flag that the node is offline.
328
-       Parameters:
329
-           updateSuccess - a boolean that is True if data request
330
-                           successful, False otherwise
331
-       Returns: nothing
332
-    """
333
-    global failedUpdateCount, nodeOnline
334
-
335
-    if updateSuccess:
336
-        failedUpdateCount = 0
337
-        # Set status and send a message to the log if the node was
338
-        # previously offline and is now online.
339
-        if not nodeOnline:
340
-            print '%s aredn node online' % getTimeStamp()
341
-            nodeOnline = True
342
-    else:
343
-        # The last attempt failed, so update the failed attempts
344
-        # count.
345
-        failedUpdateCount += 1
346
-
347
-    if failedUpdateCount > _MAX_FAILED_DATA_REQUESTS:
348
-        # Max number of failed data requests, so set
349
-        # node status to offline.
350
-        setStatusToOffline()
351
-##end def
352
-
353
-def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
354
-                lower, upper, addTrend, autoScale):
355
-    """Uses rrdtool to create a graph of specified node data item.
356
-       Parameters:
357
-           fileName - name of file containing the graph
358
-           dataItem - data item to be graphed
359
-           gLabel - string containing a graph label for the data item
360
-           gTitle - string containing a title for the graph
361
-           gStart - beginning time of the graphed data
362
-           lower - lower bound for graph ordinate #NOT USED
363
-           upper - upper bound for graph ordinate #NOT USED
364
-           addTrend - 0, show only graph data
365
-                      1, show only a trend line
366
-                      2, show a trend line and the graph data
367
-           autoScale - if True, then use vertical axis auto scaling
368
-               (lower and upper parameters are ignored), otherwise use
369
-               lower and upper parameters to set vertical axis scale
370
-       Returns: True if successful, False otherwise
371
-    """
372
-    gPath = _CHARTS_DIRECTORY + fileName + ".png"
373
-    trendWindow = { 'end-1day': 7200,
374
-                    'end-4weeks': 172800,
375
-                    'end-12months': 604800 }
376
- 
377
-    # Format the rrdtool graph command.
378
-
379
-    # Set chart start time, height, and width.
380
-    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
381
-             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
382
-   
383
-    # Set the range and scaling of the chart y-axis.
384
-    if lower < upper:
385
-        strCmd  +=  "-l %s -u %s -r " % (lower, upper)
386
-    elif autoScale:
387
-        strCmd += "-A "
388
-    strCmd += "-Y "
389
-
390
-    # Set the chart ordinate label and chart title. 
391
-    strCmd += "-v %s -t %s " % (gLabel, gTitle)
392
- 
393
-    # Show the data, or a moving average trend line over
394
-    # the data, or both.
395
-    strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem)
396
-    if addTrend == 0:
397
-        strCmd += "LINE1:dSeries#0400ff "
398
-    elif addTrend == 1:
399
-        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
400
-                  % trendWindow[gStart]
401
-    elif addTrend == 2:
402
-        strCmd += "LINE1:dSeries#0400ff "
403
-        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
404
-                  % trendWindow[gStart]
405
-     
406
-    if verboseDebug:
407
-        print "%s" % strCmd # DEBUG
408
-    
409
-    # Run the formatted rrdtool command as a subprocess.
410
-    try:
411
-        result = subprocess.check_output(strCmd, \
412
-                     stderr=subprocess.STDOUT,   \
413
-                     shell=True)
414
-    except subprocess.CalledProcessError, exError:
415
-        print "rrdtool graph failed: %s" % (exError.output)
416
-        return False
417
-
418
-    if debugOption:
419
-        print "rrdtool graph: %s\n" % result,
420
-    return True
421
-
422
-##end def
423
-
424
-def generateGraphs():
425
-    """Generate graphs for display in html documents.
426
-       Parameters: none
427
-       Returns: nothing
428
-    """
429
-    autoScale = False
430
-
431
-    # The following will force creation of charts
432
-    # of only signal strength and S/N charts.  Note that the following
433
-    # data items appear constant and do not show variation with time:
434
-    # noise level, rx mcs, rx rate, tx mcs, tx rate.  Therefore, until
435
-    # these parameters are demonstrated to vary in time, there is no point
436
-    # in creating the charts for these data items.
437
-    createAllCharts = False
438
-
439
-    # 24 hour stock charts
440
-
441
-    createGraph('24hr_signal', 'S', 'dBm', 
442
-                'RSSI\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
443
-    createGraph('24hr_snr', 'SNR', 'dB', 
444
-                'SNR\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
445
-
446
-    # 4 week stock charts
447
-
448
-    createGraph('4wk_signal', 'S', 'dBm', 
449
-                'RSSI\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
450
-    createGraph('4wk_snr', 'SNR', 'dB', 
451
-                'SNR\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
452
-
453
-    # 12 month stock charts
454
-
455
-    createGraph('12m_signal', 'S', 'dBm', 
456
-                'RSSI\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
457
-    createGraph('12m_snr', 'SNR', 'dB', 
458
-                'SNR\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
459
-
460
-    if debugOption:
461
-        #print # print a blank line to improve readability when in debug mode
462
-        pass
463
-##end def
464
-
465
-def getCLarguments():
466
-    """Get command line arguments.  There are four possible arguments
467
-          -d turns on debug mode
468
-          -v turns on verbose debug mode
469
-          -t sets the aredn node query interval
470
-          -u sets the url of the aredn nodeing device
471
-       Returns: nothing
472
-    """
473
-    global debugOption, verboseDebug, dataRequestInterval, \
474
-           arednNodeUrl
475
-
476
-    index = 1
477
-    while index < len(sys.argv):
478
-        if sys.argv[index] == '-d':
479
-            debugOption = True
480
-        elif sys.argv[index] == '-v':
481
-            debugOption = True
482
-            verboseDebug = True
483
-        elif sys.argv[index] == '-p':
484
-            try:
485
-                dataRequestInterval = abs(int(sys.argv[index + 1]))
486
-            except:
487
-                print "invalid polling period"
488
-                exit(-1)
489
-            index += 1
490
-        elif sys.argv[index] == '-u':
491
-            arednNodeUrl = sys.argv[index + 1]
492
-            index += 1
493
-        else:
494
-            cmd_name = sys.argv[0].split('/')
495
-            print "Usage: %s [-d] [-v] [-p seconds] [-u url]" % cmd_name[-1]
496
-            exit(-1)
497
-        index += 1
498
-##end def
499
-
500
-def main():
501
-    """Handles timing of events and acts as executive routine managing
502
-       all other functions.
503
-       Parameters: none
504
-       Returns: nothing
505
-    """
506
-    global dataRequestInterval
507
-
508
-    signal.signal(signal.SIGTERM, terminateAgentProcess)
509
-
510
-    print '%s starting up arednsig agent process' % \
511
-                  (getTimeStamp())
512
-
513
-    # last time output JSON file updated
514
-    lastDataRequestTime = -1
515
-    # last time charts generated
516
-    lastChartUpdateTime = - 1
517
-
518
-    ## Get command line arguments.
519
-    getCLarguments()
520
-
521
-    requestIntervalSeconds = dataRequestInterval * 60 # convert to seconds
522
-    chartUpdateIntervalSeconds = chartUpdateInterval * 60 # charts interval
523
-
524
-    ## Exit with error if rrdtool database does not exist.
525
-    if not os.path.exists(_RRD_FILE):
526
-        print 'rrdtool database does not exist\n' \
527
-              'use createArednsigRrd script to ' \
528
-              'create rrdtool database\n'
529
-        exit(1)
530
- 
531
-    ## main loop
532
-    while True:
533
-
534
-        currentTime = time.time() # get current time in seconds
535
-
536
-        # Every web update interval request data from the aredn
537
-        # node and process the received data.
538
-        if currentTime - lastDataRequestTime > requestIntervalSeconds:
539
-            lastDataRequestTime = currentTime
540
-            dData = {}
541
-            result = True
542
-
543
-            # Get the data string from the device.
544
-            sData = getArednNodeData()
545
-            # If the first http request fails, try one more time.
546
-            if sData == None:
547
-                result = False
548
-
549
-            # If successful parse the data.
550
-            if result:
551
-                result = parseNodeData(sData, dData)
552
-           
553
-            # If parse successful, write data to data files.
554
-            if result:
555
-                result = updateDatabase(dData)
556
-
557
-            if result:
558
-                writeOutputDataFile(sData, dData)
559
-
560
-            # Set the node status to online or offline depending on the
561
-            # success or failure of the above operations.
562
-            setNodeStatus(result)
563
-
564
-        # At the chart generation interval, generate charts.
565
-        if currentTime - lastChartUpdateTime > chartUpdateIntervalSeconds:
566
-            lastChartUpdateTime = currentTime
567
-            p = multiprocessing.Process(target=generateGraphs, args=())
568
-            p.start()
569
-
570
-        # Relinquish processing back to the operating system until
571
-        # the next update interval.
572
-
573
-        elapsedTime = time.time() - currentTime
574
-        if debugOption:
575
-            if result:
576
-                print "%s update successful:" % getTimeStamp(),
577
-            else:
578
-                print "%s update failed:" % getTimeStamp(),
579
-            print "%6f seconds processing time\n" % elapsedTime 
580
-        remainingTime = requestIntervalSeconds - elapsedTime
581
-        if remainingTime > 0.0:
582
-            time.sleep(remainingTime)
583
-    ## end while
584
-    return
585
-## end def
586
-
587
-if __name__ == '__main__':
588
-    try:
589
-        main()
590
-    except KeyboardInterrupt:
591
-        print '\n',
592
-        terminateAgentProcess('KeyboardInterrupt','Module')
Browse code

support for Aredn firmware version 3.20.3.0

gandolf authored on 03/31/2020 18:07:19
Showing 1 changed files
1 1
new file mode 100755
... ...
@@ -0,0 +1,592 @@
1
+#!/usr/bin/python -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: arednsigAgent.py
6
+#
7
+# Description: This module acts as an agent between the aredn node
8
+# and aredn mest services.  The agent periodically sends an http
9
+# request to the aredn node, processes the response from
10
+# the node, and performs a number of operations:
11
+#     - conversion of data items
12
+#     - update a round robin (rrdtool) database with the node data
13
+#     - periodically generate graphic charts for display in html documents
14
+#     - write the processed node status to a JSON file for use by html
15
+#       documents
16
+#
17
+# Copyright 2020 Jeff Owrey
18
+#    This program is free software: you can redistribute it and/or modify
19
+#    it under the terms of the GNU General Public License as published by
20
+#    the Free Software Foundation, either version 3 of the License, or
21
+#    (at your option) any later version.
22
+#
23
+#    This program is distributed in the hope that it will be useful,
24
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
25
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26
+#    GNU General Public License for more details.
27
+#
28
+#    You should have received a copy of the GNU General Public License
29
+#    along with this program.  If not, see http://www.gnu.org/license.
30
+#
31
+# Revision History
32
+#   * v20 released 11 Jan 2020 by J L Owrey; first release
33
+#   * v21 released 13 Feb 2020 by J L Owrey; fixed bug occuring when node
34
+#     powers on and signal data memory is empty.  Data points with N/A data
35
+#     are discarded.
36
+#   * v22 released 31 Mar 2020 by J L Owrey; upgraded for compatibility with
37
+#     Aredn firmware version 3.20.3.0.  This agent now downloads the node's
38
+#     status page and parsed the signal data from the html.
39
+#
40
+#2345678901234567890123456789012345678901234567890123456789012345678901234567890
41
+
42
+import os
43
+import urllib2
44
+import sys
45
+import signal
46
+import subprocess
47
+import multiprocessing
48
+import time
49
+
50
+_USER = os.environ['USER']
51
+_HOSTNAME = os.uname()[1]
52
+
53
+   ### DEFAULT AREDN NODE URL ###
54
+
55
+# set url of the aredn node
56
+
57
+_DEFAULT_AREDN_NODE_URL = "http://localnode:8080/cgi-bin/status"
58
+
59
+    ### FILE AND FOLDER LOCATIONS ###
60
+
61
+# folder for containing dynamic data objects
62
+_DOCROOT_PATH = "/home/%s/public_html/arednsig/" % _USER
63
+# folder for charts and output data file
64
+_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/"
65
+# location of data output file
66
+_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/arednsigOutputData.js"
67
+# dummy output data file
68
+_DUMMY_OUTPUT_FILE = _DOCROOT_PATH + "dynamic/nodeOnline.js"
69
+# database that stores node data
70
+_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
71
+
72
+    ### GLOBAL CONSTANTS ###
73
+
74
+# interval in minutes between data requests to the aredn node
75
+_DEFAULT_DATA_REQUEST_INTERVAL = 1
76
+# chart update interval in minutes
77
+_CHART_UPDATE_INTERVAL = 10
78
+
79
+# number seconds to wait for a response to HTTP request
80
+_HTTP_REQUEST_TIMEOUT = 10
81
+# max number of failed data requests allowed
82
+_MAX_FAILED_DATA_REQUESTS = 0
83
+# standard chart width in pixels
84
+_CHART_WIDTH = 600
85
+# standard chart height in pixels
86
+_CHART_HEIGHT = 150
87
+# Set this to True only if this server is intended to relay raw
88
+# node data to a mirror server.
89
+_RELAY_SERVER = False
90
+
91
+   ### GLOBAL VARIABLES ###
92
+
93
+# turn on or off of verbose debugging information
94
+debugOption = False
95
+verboseDebug = False
96
+
97
+# The following two items are used for detecting system faults
98
+# and aredn node online or offline status.
99
+
100
+# count of failed attempts to get data from aredn node
101
+failedUpdateCount = 0
102
+# detected status of aredn node device
103
+nodeOnline = True
104
+
105
+# ip address of aredn node
106
+arednNodeUrl = _DEFAULT_AREDN_NODE_URL
107
+# frequency of data requests to aredn node
108
+dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
109
+# chart update interval
110
+chartUpdateInterval = _CHART_UPDATE_INTERVAL
111
+# last node request time
112
+lastDataPointTime = -1
113
+
114
+  ###  PRIVATE METHODS  ###
115
+
116
+def getTimeStamp():
117
+    """
118
+    Set the error message time stamp to the local system time.
119
+    Parameters: none
120
+    Returns: string containing the time stamp
121
+    """
122
+    return time.strftime( "%m/%d/%Y %T", time.localtime() )
123
+##end def
124
+
125
+def getEpochSeconds(sTime):
126
+    """Convert the time stamp supplied in the weather data string
127
+       to seconds since 1/1/1970 00:00:00.
128
+       Parameters: 
129
+           sTime - the time stamp to be converted must be formatted
130
+                   as %m/%d/%Y %H:%M:%S
131
+       Returns: epoch seconds
132
+    """
133
+    try:
134
+        t_sTime = time.strptime(sTime, '%m/%d/%Y %H:%M:%S')
135
+    except Exception, exError:
136
+        print '%s getEpochSeconds: %s' % (getTimeStamp(), exError)
137
+        return None
138
+    tSeconds = int(time.mktime(t_sTime))
139
+    return tSeconds
140
+##end def
141
+
142
+def setStatusToOffline():
143
+    """Set the detected status of the aredn node to
144
+       "offline" and inform downstream clients by removing input
145
+       and output data files.
146
+       Parameters: none
147
+       Returns: nothing
148
+    """
149
+    global nodeOnline
150
+
151
+    # Inform downstream clients by removing output data file.
152
+    if os.path.exists(_OUTPUT_DATA_FILE):
153
+       os.remove(_OUTPUT_DATA_FILE)
154
+    if os.path.exists(_DUMMY_OUTPUT_FILE):
155
+       os.remove(_DUMMY_OUTPUT_FILE)
156
+    # If the aredn node was previously online, then send
157
+    # a message that we are now offline.
158
+    if nodeOnline:
159
+        print '%s aredn node offline' % getTimeStamp()
160
+    nodeOnline = False
161
+##end def
162
+
163
+def terminateAgentProcess(signal, frame):
164
+    """Send a message to log when the agent process gets killed
165
+       by the operating system.  Inform downstream clients
166
+       by removing input and output data files.
167
+       Parameters:
168
+           signal, frame - dummy parameters
169
+       Returns: nothing
170
+    """
171
+    # Inform downstream clients by removing output data file.
172
+    if os.path.exists(_OUTPUT_DATA_FILE):
173
+       os.remove(_OUTPUT_DATA_FILE)
174
+    if os.path.exists(_DUMMY_OUTPUT_FILE):
175
+       os.remove(_DUMMY_OUTPUT_FILE)
176
+    print '%s terminating arednsig agent process' % \
177
+              (getTimeStamp())
178
+    sys.exit(0)
179
+##end def
180
+
181
+  ###  PUBLIC METHODS  ###
182
+
183
+def getArednNodeData():
184
+    """Send http request to aredn node.  The response from the
185
+       node contains the node signal data as unformatted ascii text.
186
+       Parameters: none
187
+       Returns: a string containing the node signal data if successful,
188
+                or None if not successful
189
+    """
190
+    try:
191
+        conn = urllib2.urlopen(arednNodeUrl, timeout=_HTTP_REQUEST_TIMEOUT)
192
+
193
+        # Format received data into a single string.
194
+        content = ""
195
+        for line in conn:
196
+            content += line.strip()
197
+        del conn
198
+
199
+    except Exception, exError:
200
+        # If no response is received from the device, then assume that
201
+        # the device is down or unavailable over the network.  In
202
+        # that case return None to the calling function.
203
+        print "%s http error: %s" % (getTimeStamp(), exError)
204
+        return None
205
+
206
+    if verboseDebug:
207
+        print "http request successful: %d bytes" % len(content)
208
+
209
+    return content
210
+##end def
211
+
212
+def parseNodeData(sData, dData):
213
+    """Parse the node status page html from the aredn node
214
+       into its component parts.  
215
+       Parameters:
216
+           sData - the string containing the data to be parsed
217
+           dData - a dictionary object to contain the parsed data items
218
+       Returns: True if successful, False otherwise
219
+    """
220
+    try:
221
+
222
+        # Set search boundaries for signal data
223
+        strBeginSearch = '<nobr>Signal/Noise/Ratio</nobr></th>' \
224
+                         '<td valign=middle><nobr><big><b>'
225
+        strEndSearch = 'dB'
226
+        iBeginIndex = sData.find(strBeginSearch) + len(strBeginSearch)
227
+        iEndIndex = sData.find(strEndSearch, iBeginIndex)
228
+
229
+        # Exception if signal data not found
230
+        if iBeginIndex == -1 or iEndIndex == -1:
231
+            raise Exception("signal data not found in status page")
232
+
233
+        # Extract signal data from html
234
+        snr = sData[iBeginIndex:iEndIndex]
235
+        snr = snr.replace(' ','')
236
+        lsnr = snr.split('/')
237
+
238
+        # Store time and signal data in dictionary object
239
+        dData['time'] = getEpochSeconds(getTimeStamp())
240
+        dData['signal'] = lsnr[0]
241
+        dData['noise'] = lsnr[1]
242
+        dData['snr'] = lsnr[2]
243
+    
244
+    except Exception, exError:
245
+        print "%s parse failed: %s" % (getTimeStamp(), exError)
246
+        return False
247
+
248
+    if verboseDebug:
249
+        print "parse successful"
250
+    return True
251
+##end def
252
+
253
+def updateDatabase(dData):
254
+    """
255
+    Update the rrdtool database by executing an rrdtool system command.
256
+    Format the command using the data extracted from the aredn node
257
+    response.   
258
+    Parameters: dData - dictionary object containing data items to be
259
+                        written to the rr database file
260
+    Returns: True if successful, False otherwise
261
+    """
262
+    # Format the rrdtool update command.
263
+    strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s:%s:%s"
264
+    strCmd = strFmt % (_RRD_FILE, dData['time'], dData['signal'], \
265
+             dData['noise'], dData['snr'], '0', \
266
+             '0', '0', '0')
267
+
268
+    if verboseDebug:
269
+        print "%s" % strCmd # DEBUG
270
+
271
+    # Run the command as a subprocess.
272
+    try:
273
+        subprocess.check_output(strCmd, shell=True,  \
274
+                             stderr=subprocess.STDOUT)
275
+    except subprocess.CalledProcessError, exError:
276
+        print "%s: rrdtool update failed: %s" % \
277
+                    (getTimeStamp(), exError.output)
278
+        return False
279
+
280
+    return True
281
+##end def
282
+
283
+def writeOutputDataFile(sData, dData):
284
+    """Write node data items to the output data file, formatted as 
285
+       a Javascript file.  This file may then be accessed and used by
286
+       by downstream clients, for instance, in HTML documents.
287
+       Parameters:
288
+           sData - a string object containing the data to be written
289
+                   to the output data file
290
+       Returns: True if successful, False otherwise
291
+    """
292
+    # Write file for use by html clients.  The following two
293
+    # data items are sent to the client file.
294
+    #    * The last database update date and time
295
+    #    * The data request interval
296
+    lastUpdate = time.strftime( "%m.%d.%Y %T", 
297
+                                time.localtime(dData['time']) )
298
+    sDate = "[{\"date\":\"%s\",\"period\":\"%s\"}]" % \
299
+           (lastUpdate, chartUpdateInterval)
300
+    try:
301
+        fc = open(_DUMMY_OUTPUT_FILE, "w")
302
+        fc.write(sDate)
303
+        fc.close()
304
+    except Exception, exError:
305
+        print "%s write node file failed: %s" % (getTimeStamp(), exError)
306
+        return False
307
+
308
+    if _RELAY_SERVER:
309
+        # Write the entire node data response to the output data file.
310
+        try:
311
+            fc = open(_OUTPUT_DATA_FILE, "w")
312
+            fc.write(sData)
313
+            fc.close()
314
+        except Exception, exError:
315
+            print "%s write output file failed: %s" % \
316
+                  (getTimeStamp(), exError)
317
+            return False
318
+        if verboseDebug:
319
+            print "write output data file: %d bytes" % len(sData)
320
+
321
+    return True
322
+## end def
323
+
324
+def setNodeStatus(updateSuccess):
325
+    """Detect if aredn node is offline or not available on
326
+       the network. After a set number of attempts to get data
327
+       from the node set a flag that the node is offline.
328
+       Parameters:
329
+           updateSuccess - a boolean that is True if data request
330
+                           successful, False otherwise
331
+       Returns: nothing
332
+    """
333
+    global failedUpdateCount, nodeOnline
334
+
335
+    if updateSuccess:
336
+        failedUpdateCount = 0
337
+        # Set status and send a message to the log if the node was
338
+        # previously offline and is now online.
339
+        if not nodeOnline:
340
+            print '%s aredn node online' % getTimeStamp()
341
+            nodeOnline = True
342
+    else:
343
+        # The last attempt failed, so update the failed attempts
344
+        # count.
345
+        failedUpdateCount += 1
346
+
347
+    if failedUpdateCount > _MAX_FAILED_DATA_REQUESTS:
348
+        # Max number of failed data requests, so set
349
+        # node status to offline.
350
+        setStatusToOffline()
351
+##end def
352
+
353
+def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
354
+                lower, upper, addTrend, autoScale):
355
+    """Uses rrdtool to create a graph of specified node data item.
356
+       Parameters:
357
+           fileName - name of file containing the graph
358
+           dataItem - data item to be graphed
359
+           gLabel - string containing a graph label for the data item
360
+           gTitle - string containing a title for the graph
361
+           gStart - beginning time of the graphed data
362
+           lower - lower bound for graph ordinate #NOT USED
363
+           upper - upper bound for graph ordinate #NOT USED
364
+           addTrend - 0, show only graph data
365
+                      1, show only a trend line
366
+                      2, show a trend line and the graph data
367
+           autoScale - if True, then use vertical axis auto scaling
368
+               (lower and upper parameters are ignored), otherwise use
369
+               lower and upper parameters to set vertical axis scale
370
+       Returns: True if successful, False otherwise
371
+    """
372
+    gPath = _CHARTS_DIRECTORY + fileName + ".png"
373
+    trendWindow = { 'end-1day': 7200,
374
+                    'end-4weeks': 172800,
375
+                    'end-12months': 604800 }
376
+ 
377
+    # Format the rrdtool graph command.
378
+
379
+    # Set chart start time, height, and width.
380
+    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
381
+             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
382
+   
383
+    # Set the range and scaling of the chart y-axis.
384
+    if lower < upper:
385
+        strCmd  +=  "-l %s -u %s -r " % (lower, upper)
386
+    elif autoScale:
387
+        strCmd += "-A "
388
+    strCmd += "-Y "
389
+
390
+    # Set the chart ordinate label and chart title. 
391
+    strCmd += "-v %s -t %s " % (gLabel, gTitle)
392
+ 
393
+    # Show the data, or a moving average trend line over
394
+    # the data, or both.
395
+    strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem)
396
+    if addTrend == 0:
397
+        strCmd += "LINE1:dSeries#0400ff "
398
+    elif addTrend == 1:
399
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
400
+                  % trendWindow[gStart]
401
+    elif addTrend == 2:
402
+        strCmd += "LINE1:dSeries#0400ff "
403
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
404
+                  % trendWindow[gStart]
405
+     
406
+    if verboseDebug:
407
+        print "%s" % strCmd # DEBUG
408
+    
409
+    # Run the formatted rrdtool command as a subprocess.
410
+    try:
411
+        result = subprocess.check_output(strCmd, \
412
+                     stderr=subprocess.STDOUT,   \
413
+                     shell=True)
414
+    except subprocess.CalledProcessError, exError:
415
+        print "rrdtool graph failed: %s" % (exError.output)
416
+        return False
417
+
418
+    if debugOption:
419
+        print "rrdtool graph: %s\n" % result,
420
+    return True
421
+
422
+##end def
423
+
424
+def generateGraphs():
425
+    """Generate graphs for display in html documents.
426
+       Parameters: none
427
+       Returns: nothing
428
+    """
429
+    autoScale = False
430
+
431
+    # The following will force creation of charts
432
+    # of only signal strength and S/N charts.  Note that the following
433
+    # data items appear constant and do not show variation with time:
434
+    # noise level, rx mcs, rx rate, tx mcs, tx rate.  Therefore, until
435
+    # these parameters are demonstrated to vary in time, there is no point
436
+    # in creating the charts for these data items.
437
+    createAllCharts = False
438
+
439
+    # 24 hour stock charts
440
+
441
+    createGraph('24hr_signal', 'S', 'dBm', 
442
+                'RSSI\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
443
+    createGraph('24hr_snr', 'SNR', 'dB', 
444
+                'SNR\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
445
+
446
+    # 4 week stock charts
447
+
448
+    createGraph('4wk_signal', 'S', 'dBm', 
449
+                'RSSI\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
450
+    createGraph('4wk_snr', 'SNR', 'dB', 
451
+                'SNR\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
452
+
453
+    # 12 month stock charts
454
+
455
+    createGraph('12m_signal', 'S', 'dBm', 
456
+                'RSSI\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
457
+    createGraph('12m_snr', 'SNR', 'dB', 
458
+                'SNR\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
459
+
460
+    if debugOption:
461
+        #print # print a blank line to improve readability when in debug mode
462
+        pass
463
+##end def
464
+
465
+def getCLarguments():
466
+    """Get command line arguments.  There are four possible arguments
467
+          -d turns on debug mode
468
+          -v turns on verbose debug mode
469
+          -t sets the aredn node query interval
470
+          -u sets the url of the aredn nodeing device
471
+       Returns: nothing
472
+    """
473
+    global debugOption, verboseDebug, dataRequestInterval, \
474
+           arednNodeUrl
475
+
476
+    index = 1
477
+    while index < len(sys.argv):
478
+        if sys.argv[index] == '-d':
479
+            debugOption = True
480
+        elif sys.argv[index] == '-v':
481
+            debugOption = True
482
+            verboseDebug = True
483
+        elif sys.argv[index] == '-p':
484
+            try:
485
+                dataRequestInterval = abs(int(sys.argv[index + 1]))
486
+            except:
487
+                print "invalid polling period"
488
+                exit(-1)
489
+            index += 1
490
+        elif sys.argv[index] == '-u':
491
+            arednNodeUrl = sys.argv[index + 1]
492
+            index += 1
493
+        else:
494
+            cmd_name = sys.argv[0].split('/')
495
+            print "Usage: %s [-d] [-v] [-p seconds] [-u url]" % cmd_name[-1]
496
+            exit(-1)
497
+        index += 1
498
+##end def
499
+
500
+def main():
501
+    """Handles timing of events and acts as executive routine managing
502
+       all other functions.
503
+       Parameters: none
504
+       Returns: nothing
505
+    """
506
+    global dataRequestInterval
507
+
508
+    signal.signal(signal.SIGTERM, terminateAgentProcess)
509
+
510
+    print '%s starting up arednsig agent process' % \
511
+                  (getTimeStamp())
512
+
513
+    # last time output JSON file updated
514
+    lastDataRequestTime = -1
515
+    # last time charts generated
516
+    lastChartUpdateTime = - 1
517
+
518
+    ## Get command line arguments.
519
+    getCLarguments()
520
+
521
+    requestIntervalSeconds = dataRequestInterval * 60 # convert to seconds
522
+    chartUpdateIntervalSeconds = chartUpdateInterval * 60 # charts interval
523
+
524
+    ## Exit with error if rrdtool database does not exist.
525
+    if not os.path.exists(_RRD_FILE):
526
+        print 'rrdtool database does not exist\n' \
527
+              'use createArednsigRrd script to ' \
528
+              'create rrdtool database\n'
529
+        exit(1)
530
+ 
531
+    ## main loop
532
+    while True:
533
+
534
+        currentTime = time.time() # get current time in seconds
535
+
536
+        # Every web update interval request data from the aredn
537
+        # node and process the received data.
538
+        if currentTime - lastDataRequestTime > requestIntervalSeconds:
539
+            lastDataRequestTime = currentTime
540
+            dData = {}
541
+            result = True
542
+
543
+            # Get the data string from the device.
544
+            sData = getArednNodeData()
545
+            # If the first http request fails, try one more time.
546
+            if sData == None:
547
+                result = False
548
+
549
+            # If successful parse the data.
550
+            if result:
551
+                result = parseNodeData(sData, dData)
552
+           
553
+            # If parse successful, write data to data files.
554
+            if result:
555
+                result = updateDatabase(dData)
556
+
557
+            if result:
558
+                writeOutputDataFile(sData, dData)
559
+
560
+            # Set the node status to online or offline depending on the
561
+            # success or failure of the above operations.
562
+            setNodeStatus(result)
563
+
564
+        # At the chart generation interval, generate charts.
565
+        if currentTime - lastChartUpdateTime > chartUpdateIntervalSeconds:
566
+            lastChartUpdateTime = currentTime
567
+            p = multiprocessing.Process(target=generateGraphs, args=())
568
+            p.start()
569
+
570
+        # Relinquish processing back to the operating system until
571
+        # the next update interval.
572
+
573
+        elapsedTime = time.time() - currentTime
574
+        if debugOption:
575
+            if result:
576
+                print "%s update successful:" % getTimeStamp(),
577
+            else:
578
+                print "%s update failed:" % getTimeStamp(),
579
+            print "%6f seconds processing time\n" % elapsedTime 
580
+        remainingTime = requestIntervalSeconds - elapsedTime
581
+        if remainingTime > 0.0:
582
+            time.sleep(remainingTime)
583
+    ## end while
584
+    return
585
+## end def
586
+
587
+if __name__ == '__main__':
588
+    try:
589
+        main()
590
+    except KeyboardInterrupt:
591
+        print '\n',
592
+        terminateAgentProcess('KeyboardInterrupt','Module')
Browse code

fix version numbers

gandolf authored on 03/31/2020 18:04:12
Showing 1 changed files
1 1
deleted file mode 100755
... ...
@@ -1,592 +0,0 @@
1
-#!/usr/bin/python -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: arednsigAgent.py
6
-#
7
-# Description: This module acts as an agent between the aredn node
8
-# and aredn mest services.  The agent periodically sends an http
9
-# request to the aredn node, processes the response from
10
-# the node, and performs a number of operations:
11
-#     - conversion of data items
12
-#     - update a round robin (rrdtool) database with the node data
13
-#     - periodically generate graphic charts for display in html documents
14
-#     - write the processed node status to a JSON file for use by html
15
-#       documents
16
-#
17
-# Copyright 2020 Jeff Owrey
18
-#    This program is free software: you can redistribute it and/or modify
19
-#    it under the terms of the GNU General Public License as published by
20
-#    the Free Software Foundation, either version 3 of the License, or
21
-#    (at your option) any later version.
22
-#
23
-#    This program is distributed in the hope that it will be useful,
24
-#    but WITHOUT ANY WARRANTY; without even the implied warranty of
25
-#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26
-#    GNU General Public License for more details.
27
-#
28
-#    You should have received a copy of the GNU General Public License
29
-#    along with this program.  If not, see http://www.gnu.org/license.
30
-#
31
-# Revision History
32
-#   * v20 released 11 Jan 2020 by J L Owrey; first release
33
-#   * v21 released 13 Feb 2020 by J L Owrey; fixed bug occuring when node
34
-#     powers on and signal data memory is empty.  Data points with N/A data
35
-#     are discarded.
36
-#   * v22 released 31 Mar 2020 by J L Owrey; upgraded for compatibility with
37
-#     Aredn firmware version 3.20.3.0.  This agent now downloads the node's
38
-#     status page and parsed the signal data from the html.
39
-#
40
-#2345678901234567890123456789012345678901234567890123456789012345678901234567890
41
-
42
-import os
43
-import urllib2
44
-import sys
45
-import signal
46
-import subprocess
47
-import multiprocessing
48
-import time
49
-
50
-_USER = os.environ['USER']
51
-_HOSTNAME = os.uname()[1]
52
-
53
-   ### DEFAULT AREDN NODE URL ###
54
-
55
-# set url of the aredn node
56
-
57
-_DEFAULT_AREDN_NODE_URL = "http://localnode:8080/cgi-bin/status"
58
-
59
-    ### FILE AND FOLDER LOCATIONS ###
60
-
61
-# folder for containing dynamic data objects
62
-_DOCROOT_PATH = "/home/%s/public_html/arednsig/" % _USER
63
-# folder for charts and output data file
64
-_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/"
65
-# location of data output file
66
-_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/arednsigOutputData.js"
67
-# dummy output data file
68
-_DUMMY_OUTPUT_FILE = _DOCROOT_PATH + "dynamic/nodeOnline.js"
69
-# database that stores node data
70
-_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
71
-
72
-    ### GLOBAL CONSTANTS ###
73
-
74
-# interval in minutes between data requests to the aredn node
75
-_DEFAULT_DATA_REQUEST_INTERVAL = 1
76
-# chart update interval in minutes
77
-_CHART_UPDATE_INTERVAL = 10
78
-
79
-# number seconds to wait for a response to HTTP request
80
-_HTTP_REQUEST_TIMEOUT = 10
81
-# max number of failed data requests allowed
82
-_MAX_FAILED_DATA_REQUESTS = 0
83
-# standard chart width in pixels
84
-_CHART_WIDTH = 600
85
-# standard chart height in pixels
86
-_CHART_HEIGHT = 150
87
-# Set this to True only if this server is intended to relay raw
88
-# node data to a mirror server.
89
-_RELAY_SERVER = False
90
-
91
-   ### GLOBAL VARIABLES ###
92
-
93
-# turn on or off of verbose debugging information
94
-debugOption = False
95
-verboseDebug = False
96
-
97
-# The following two items are used for detecting system faults
98
-# and aredn node online or offline status.
99
-
100
-# count of failed attempts to get data from aredn node
101
-failedUpdateCount = 0
102
-# detected status of aredn node device
103
-nodeOnline = True
104
-
105
-# ip address of aredn node
106
-arednNodeUrl = _DEFAULT_AREDN_NODE_URL
107
-# frequency of data requests to aredn node
108
-dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
109
-# chart update interval
110
-chartUpdateInterval = _CHART_UPDATE_INTERVAL
111
-# last node request time
112
-lastDataPointTime = -1
113
-
114
-  ###  PRIVATE METHODS  ###
115
-
116
-def getTimeStamp():
117
-    """
118
-    Set the error message time stamp to the local system time.
119
-    Parameters: none
120
-    Returns: string containing the time stamp
121
-    """
122
-    return time.strftime( "%m/%d/%Y %T", time.localtime() )
123
-##end def
124
-
125
-def getEpochSeconds(sTime):
126
-    """Convert the time stamp supplied in the weather data string
127
-       to seconds since 1/1/1970 00:00:00.
128
-       Parameters: 
129
-           sTime - the time stamp to be converted must be formatted
130
-                   as %m/%d/%Y %H:%M:%S
131
-       Returns: epoch seconds
132
-    """
133
-    try:
134
-        t_sTime = time.strptime(sTime, '%m/%d/%Y %H:%M:%S')
135
-    except Exception, exError:
136
-        print '%s getEpochSeconds: %s' % (getTimeStamp(), exError)
137
-        return None
138
-    tSeconds = int(time.mktime(t_sTime))
139
-    return tSeconds
140
-##end def
141
-
142
-def setStatusToOffline():
143
-    """Set the detected status of the aredn node to
144
-       "offline" and inform downstream clients by removing input
145
-       and output data files.
146
-       Parameters: none
147
-       Returns: nothing
148
-    """
149
-    global nodeOnline
150
-
151
-    # Inform downstream clients by removing output data file.
152
-    if os.path.exists(_OUTPUT_DATA_FILE):
153
-       os.remove(_OUTPUT_DATA_FILE)
154
-    if os.path.exists(_DUMMY_OUTPUT_FILE):
155
-       os.remove(_DUMMY_OUTPUT_FILE)
156
-    # If the aredn node was previously online, then send
157
-    # a message that we are now offline.
158
-    if nodeOnline:
159
-        print '%s aredn node offline' % getTimeStamp()
160
-    nodeOnline = False
161
-##end def
162
-
163
-def terminateAgentProcess(signal, frame):
164
-    """Send a message to log when the agent process gets killed
165
-       by the operating system.  Inform downstream clients
166
-       by removing input and output data files.
167
-       Parameters:
168
-           signal, frame - dummy parameters
169
-       Returns: nothing
170
-    """
171
-    # Inform downstream clients by removing output data file.
172
-    if os.path.exists(_OUTPUT_DATA_FILE):
173
-       os.remove(_OUTPUT_DATA_FILE)
174
-    if os.path.exists(_DUMMY_OUTPUT_FILE):
175
-       os.remove(_DUMMY_OUTPUT_FILE)
176
-    print '%s terminating arednsig agent process' % \
177
-              (getTimeStamp())
178
-    sys.exit(0)
179
-##end def
180
-
181
-  ###  PUBLIC METHODS  ###
182
-
183
-def getArednNodeData():
184
-    """Send http request to aredn node.  The response from the
185
-       node contains the node signal data as unformatted ascii text.
186
-       Parameters: none
187
-       Returns: a string containing the node signal data if successful,
188
-                or None if not successful
189
-    """
190
-    try:
191
-        conn = urllib2.urlopen(arednNodeUrl, timeout=_HTTP_REQUEST_TIMEOUT)
192
-
193
-        # Format received data into a single string.
194
-        content = ""
195
-        for line in conn:
196
-            content += line.strip()
197
-        del conn
198
-
199
-    except Exception, exError:
200
-        # If no response is received from the device, then assume that
201
-        # the device is down or unavailable over the network.  In
202
-        # that case return None to the calling function.
203
-        print "%s http error: %s" % (getTimeStamp(), exError)
204
-        return None
205
-
206
-    if verboseDebug:
207
-        print "http request successful: %d bytes" % len(content)
208
-
209
-    return content
210
-##end def
211
-
212
-def parseNodeData(sData, dData):
213
-    """Parse the node status page html from the aredn node
214
-       into its component parts.  
215
-       Parameters:
216
-           sData - the string containing the data to be parsed
217
-           dData - a dictionary object to contain the parsed data items
218
-       Returns: True if successful, False otherwise
219
-    """
220
-    try:
221
-
222
-        # Set search boundaries for signal data
223
-        strBeginSearch = '<nobr>Signal/Noise/Ratio</nobr></th>' \
224
-                         '<td valign=middle><nobr><big><b>'
225
-        strEndSearch = 'dB'
226
-        iBeginIndex = sData.find(strBeginSearch) + len(strBeginSearch)
227
-        iEndIndex = sData.find(strEndSearch, iBeginIndex)
228
-
229
-        # Exception if signal data not found
230
-        if iBeginIndex == -1 or iEndIndex == -1:
231
-            raise Exception("signal data not found in status page")
232
-
233
-        # Extract signal data from html
234
-        snr = sData[iBeginIndex:iEndIndex]
235
-        snr = snr.replace(' ','')
236
-        lsnr = snr.split('/')
237
-
238
-        # Store time and signal data in dictionary object
239
-        dData['time'] = getEpochSeconds(getTimeStamp())
240
-        dData['signal'] = lsnr[0]
241
-        dData['noise'] = lsnr[1]
242
-        dData['snr'] = lsnr[2]
243
-    
244
-    except Exception, exError:
245
-        print "%s parse failed: %s" % (getTimeStamp(), exError)
246
-        return False
247
-
248
-    if verboseDebug:
249
-        print "parse successful"
250
-    return True
251
-##end def
252
-
253
-def updateDatabase(dData):
254
-    """
255
-    Update the rrdtool database by executing an rrdtool system command.
256
-    Format the command using the data extracted from the aredn node
257
-    response.   
258
-    Parameters: dData - dictionary object containing data items to be
259
-                        written to the rr database file
260
-    Returns: True if successful, False otherwise
261
-    """
262
-    # Format the rrdtool update command.
263
-    strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s:%s:%s"
264
-    strCmd = strFmt % (_RRD_FILE, dData['time'], dData['signal'], \
265
-             dData['noise'], dData['snr'], '0', \
266
-             '0', '0', '0')
267
-
268
-    if verboseDebug:
269
-        print "%s" % strCmd # DEBUG
270
-
271
-    # Run the command as a subprocess.
272
-    try:
273
-        subprocess.check_output(strCmd, shell=True,  \
274
-                             stderr=subprocess.STDOUT)
275
-    except subprocess.CalledProcessError, exError:
276
-        print "%s: rrdtool update failed: %s" % \
277
-                    (getTimeStamp(), exError.output)
278
-        return False
279
-
280
-    return True
281
-##end def
282
-
283
-def writeOutputDataFile(sData, dData):
284
-    """Write node data items to the output data file, formatted as 
285
-       a Javascript file.  This file may then be accessed and used by
286
-       by downstream clients, for instance, in HTML documents.
287
-       Parameters:
288
-           sData - a string object containing the data to be written
289
-                   to the output data file
290
-       Returns: True if successful, False otherwise
291
-    """
292
-    # Write file for use by html clients.  The following two
293
-    # data items are sent to the client file.
294
-    #    * The last database update date and time
295
-    #    * The data request interval
296
-    lastUpdate = time.strftime( "%m.%d.%Y %T", 
297
-                                time.localtime(dData['time']) )
298
-    sDate = "[{\"date\":\"%s\",\"period\":\"%s\"}]" % \
299
-           (lastUpdate, chartUpdateInterval)
300
-    try:
301
-        fc = open(_DUMMY_OUTPUT_FILE, "w")
302
-        fc.write(sDate)
303
-        fc.close()
304
-    except Exception, exError:
305
-        print "%s write node file failed: %s" % (getTimeStamp(), exError)
306
-        return False
307
-
308
-    if _RELAY_SERVER:
309
-        # Write the entire node data response to the output data file.
310
-        try:
311
-            fc = open(_OUTPUT_DATA_FILE, "w")
312
-            fc.write(sData)
313
-            fc.close()
314
-        except Exception, exError:
315
-            print "%s write output file failed: %s" % \
316
-                  (getTimeStamp(), exError)
317
-            return False
318
-        if verboseDebug:
319
-            print "write output data file: %d bytes" % len(sData)
320
-
321
-    return True
322
-## end def
323
-
324
-def setNodeStatus(updateSuccess):
325
-    """Detect if aredn node is offline or not available on
326
-       the network. After a set number of attempts to get data
327
-       from the node set a flag that the node is offline.
328
-       Parameters:
329
-           updateSuccess - a boolean that is True if data request
330
-                           successful, False otherwise
331
-       Returns: nothing
332
-    """
333
-    global failedUpdateCount, nodeOnline
334
-
335
-    if updateSuccess:
336
-        failedUpdateCount = 0
337
-        # Set status and send a message to the log if the node was
338
-        # previously offline and is now online.
339
-        if not nodeOnline:
340
-            print '%s aredn node online' % getTimeStamp()
341
-            nodeOnline = True
342
-    else:
343
-        # The last attempt failed, so update the failed attempts
344
-        # count.
345
-        failedUpdateCount += 1
346
-
347
-    if failedUpdateCount > _MAX_FAILED_DATA_REQUESTS:
348
-        # Max number of failed data requests, so set
349
-        # node status to offline.
350
-        setStatusToOffline()
351
-##end def
352
-
353
-def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
354
-                lower, upper, addTrend, autoScale):
355
-    """Uses rrdtool to create a graph of specified node data item.
356
-       Parameters:
357
-           fileName - name of file containing the graph
358
-           dataItem - data item to be graphed
359
-           gLabel - string containing a graph label for the data item
360
-           gTitle - string containing a title for the graph
361
-           gStart - beginning time of the graphed data
362
-           lower - lower bound for graph ordinate #NOT USED
363
-           upper - upper bound for graph ordinate #NOT USED
364
-           addTrend - 0, show only graph data
365
-                      1, show only a trend line
366
-                      2, show a trend line and the graph data
367
-           autoScale - if True, then use vertical axis auto scaling
368
-               (lower and upper parameters are ignored), otherwise use
369
-               lower and upper parameters to set vertical axis scale
370
-       Returns: True if successful, False otherwise
371
-    """
372
-    gPath = _CHARTS_DIRECTORY + fileName + ".png"
373
-    trendWindow = { 'end-1day': 7200,
374
-                    'end-4weeks': 172800,
375
-                    'end-12months': 604800 }
376
- 
377
-    # Format the rrdtool graph command.
378
-
379
-    # Set chart start time, height, and width.
380
-    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
381
-             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
382
-   
383
-    # Set the range and scaling of the chart y-axis.
384
-    if lower < upper:
385
-        strCmd  +=  "-l %s -u %s -r " % (lower, upper)
386
-    elif autoScale:
387
-        strCmd += "-A "
388
-    strCmd += "-Y "
389
-
390
-    # Set the chart ordinate label and chart title. 
391
-    strCmd += "-v %s -t %s " % (gLabel, gTitle)
392
- 
393
-    # Show the data, or a moving average trend line over
394
-    # the data, or both.
395
-    strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem)
396
-    if addTrend == 0:
397
-        strCmd += "LINE1:dSeries#0400ff "
398
-    elif addTrend == 1:
399
-        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
400
-                  % trendWindow[gStart]
401
-    elif addTrend == 2:
402
-        strCmd += "LINE1:dSeries#0400ff "
403
-        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
404
-                  % trendWindow[gStart]
405
-     
406
-    if verboseDebug:
407
-        print "%s" % strCmd # DEBUG
408
-    
409
-    # Run the formatted rrdtool command as a subprocess.
410
-    try:
411
-        result = subprocess.check_output(strCmd, \
412
-                     stderr=subprocess.STDOUT,   \
413
-                     shell=True)
414
-    except subprocess.CalledProcessError, exError:
415
-        print "rrdtool graph failed: %s" % (exError.output)
416
-        return False
417
-
418
-    if debugOption:
419
-        print "rrdtool graph: %s\n" % result,
420
-    return True
421
-
422
-##end def
423
-
424
-def generateGraphs():
425
-    """Generate graphs for display in html documents.
426
-       Parameters: none
427
-       Returns: nothing
428
-    """
429
-    autoScale = False
430
-
431
-    # The following will force creation of charts
432
-    # of only signal strength and S/N charts.  Note that the following
433
-    # data items appear constant and do not show variation with time:
434
-    # noise level, rx mcs, rx rate, tx mcs, tx rate.  Therefore, until
435
-    # these parameters are demonstrated to vary in time, there is no point
436
-    # in creating the charts for these data items.
437
-    createAllCharts = False
438
-
439
-    # 24 hour stock charts
440
-
441
-    createGraph('24hr_signal', 'S', 'dBm', 
442
-                'RSSI\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
443
-    createGraph('24hr_snr', 'SNR', 'dB', 
444
-                'SNR\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
445
-
446
-    # 4 week stock charts
447
-
448
-    createGraph('4wk_signal', 'S', 'dBm', 
449
-                'RSSI\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
450
-    createGraph('4wk_snr', 'SNR', 'dB', 
451
-                'SNR\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
452
-
453
-    # 12 month stock charts
454
-
455
-    createGraph('12m_signal', 'S', 'dBm', 
456
-                'RSSI\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
457
-    createGraph('12m_snr', 'SNR', 'dB', 
458
-                'SNR\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
459
-
460
-    if debugOption:
461
-        #print # print a blank line to improve readability when in debug mode
462
-        pass
463
-##end def
464
-
465
-def getCLarguments():
466
-    """Get command line arguments.  There are four possible arguments
467
-          -d turns on debug mode
468
-          -v turns on verbose debug mode
469
-          -t sets the aredn node query interval
470
-          -u sets the url of the aredn nodeing device
471
-       Returns: nothing
472
-    """
473
-    global debugOption, verboseDebug, dataRequestInterval, \
474
-           arednNodeUrl
475
-
476
-    index = 1
477
-    while index < len(sys.argv):
478
-        if sys.argv[index] == '-d':
479
-            debugOption = True
480
-        elif sys.argv[index] == '-v':
481
-            debugOption = True
482
-            verboseDebug = True
483
-        elif sys.argv[index] == '-p':
484
-            try:
485
-                dataRequestInterval = abs(int(sys.argv[index + 1]))
486
-            except:
487
-                print "invalid polling period"
488
-                exit(-1)
489
-            index += 1
490
-        elif sys.argv[index] == '-u':
491
-            arednNodeUrl = sys.argv[index + 1]
492
-            index += 1
493
-        else:
494
-            cmd_name = sys.argv[0].split('/')
495
-            print "Usage: %s [-d] [-v] [-p seconds] [-u url]" % cmd_name[-1]
496
-            exit(-1)
497
-        index += 1
498
-##end def
499
-
500
-def main():
501
-    """Handles timing of events and acts as executive routine managing
502
-       all other functions.
503
-       Parameters: none
504
-       Returns: nothing
505
-    """
506
-    global dataRequestInterval
507
-
508
-    signal.signal(signal.SIGTERM, terminateAgentProcess)
509
-
510
-    print '%s starting up arednsig agent process' % \
511
-                  (getTimeStamp())
512
-
513
-    # last time output JSON file updated
514
-    lastDataRequestTime = -1
515
-    # last time charts generated
516
-    lastChartUpdateTime = - 1
517
-
518
-    ## Get command line arguments.
519
-    getCLarguments()
520
-
521
-    requestIntervalSeconds = dataRequestInterval * 60 # convert to seconds
522
-    chartUpdateIntervalSeconds = chartUpdateInterval * 60 # charts interval
523
-
524
-    ## Exit with error if rrdtool database does not exist.
525
-    if not os.path.exists(_RRD_FILE):
526
-        print 'rrdtool database does not exist\n' \
527
-              'use createArednsigRrd script to ' \
528
-              'create rrdtool database\n'
529
-        exit(1)
530
- 
531
-    ## main loop
532
-    while True:
533
-
534
-        currentTime = time.time() # get current time in seconds
535
-
536
-        # Every web update interval request data from the aredn
537
-        # node and process the received data.
538
-        if currentTime - lastDataRequestTime > requestIntervalSeconds:
539
-            lastDataRequestTime = currentTime
540
-            dData = {}
541
-            result = True
542
-
543
-            # Get the data string from the device.
544
-            sData = getArednNodeData()
545
-            # If the first http request fails, try one more time.
546
-            if sData == None:
547
-                result = False
548
-
549
-            # If successful parse the data.
550
-            if result:
551
-                result = parseNodeData(sData, dData)
552
-           
553
-            # If parse successful, write data to data files.
554
-            if result:
555
-                result = updateDatabase(dData)
556
-
557
-            if result:
558
-                writeOutputDataFile(sData, dData)
559
-
560
-            # Set the node status to online or offline depending on the
561
-            # success or failure of the above operations.
562
-            setNodeStatus(result)
563
-
564
-        # At the chart generation interval, generate charts.
565
-        if currentTime - lastChartUpdateTime > chartUpdateIntervalSeconds:
566
-            lastChartUpdateTime = currentTime
567
-            p = multiprocessing.Process(target=generateGraphs, args=())
568
-            p.start()
569
-
570
-        # Relinquish processing back to the operating system until
571
-        # the next update interval.
572
-
573
-        elapsedTime = time.time() - currentTime
574
-        if debugOption:
575
-            if result:
576
-                print "%s update successful:" % getTimeStamp(),
577
-            else:
578
-                print "%s update failed:" % getTimeStamp(),
579
-            print "%6f seconds processing time\n" % elapsedTime 
580
-        remainingTime = requestIntervalSeconds - elapsedTime
581
-        if remainingTime > 0.0:
582
-            time.sleep(remainingTime)
583
-    ## end while
584
-    return
585
-## end def
586
-
587
-if __name__ == '__main__':
588
-    try:
589
-        main()
590
-    except KeyboardInterrupt:
591
-        print '\n',
592
-        terminateAgentProcess('KeyboardInterrupt','Module')
Browse code

support for Aredn firmware version 3.20.3.0

gandolf authored on 03/31/2020 18:01:47
Showing 1 changed files
... ...
@@ -33,6 +33,9 @@
33 33
 #   * v21 released 13 Feb 2020 by J L Owrey; fixed bug occuring when node
34 34
 #     powers on and signal data memory is empty.  Data points with N/A data
35 35
 #     are discarded.
36
+#   * v22 released 31 Mar 2020 by J L Owrey; upgraded for compatibility with
37
+#     Aredn firmware version 3.20.3.0.  This agent now downloads the node's
38
+#     status page and parsed the signal data from the html.
36 39
 #
37 40
 #2345678901234567890123456789012345678901234567890123456789012345678901234567890
38 41
 
Browse code

support for Aredn FW v3.19.3.0

gandolf authored on 03/31/2020 17:38:41
Showing 1 changed files
1 1
new file mode 100755
... ...
@@ -0,0 +1,589 @@
1
+#!/usr/bin/python -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: arednsigAgent.py
6
+#
7
+# Description: This module acts as an agent between the aredn node
8
+# and aredn mest services.  The agent periodically sends an http
9
+# request to the aredn node, processes the response from
10
+# the node, and performs a number of operations:
11
+#     - conversion of data items
12
+#     - update a round robin (rrdtool) database with the node data
13
+#     - periodically generate graphic charts for display in html documents
14
+#     - write the processed node status to a JSON file for use by html
15
+#       documents
16
+#
17
+# Copyright 2020 Jeff Owrey
18
+#    This program is free software: you can redistribute it and/or modify
19
+#    it under the terms of the GNU General Public License as published by
20
+#    the Free Software Foundation, either version 3 of the License, or
21
+#    (at your option) any later version.
22
+#
23
+#    This program is distributed in the hope that it will be useful,
24
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
25
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26
+#    GNU General Public License for more details.
27
+#
28
+#    You should have received a copy of the GNU General Public License
29
+#    along with this program.  If not, see http://www.gnu.org/license.
30
+#
31
+# Revision History
32
+#   * v20 released 11 Jan 2020 by J L Owrey; first release
33
+#   * v21 released 13 Feb 2020 by J L Owrey; fixed bug occuring when node
34
+#     powers on and signal data memory is empty.  Data points with N/A data
35
+#     are discarded.
36
+#
37
+#2345678901234567890123456789012345678901234567890123456789012345678901234567890
38
+
39
+import os
40
+import urllib2
41
+import sys
42
+import signal
43
+import subprocess
44
+import multiprocessing
45
+import time
46
+
47
+_USER = os.environ['USER']
48
+_HOSTNAME = os.uname()[1]
49
+
50
+   ### DEFAULT AREDN NODE URL ###
51
+
52
+# set url of the aredn node
53
+
54
+_DEFAULT_AREDN_NODE_URL = "http://localnode:8080/cgi-bin/status"
55
+
56
+    ### FILE AND FOLDER LOCATIONS ###
57
+
58
+# folder for containing dynamic data objects
59
+_DOCROOT_PATH = "/home/%s/public_html/arednsig/" % _USER
60
+# folder for charts and output data file
61
+_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/"
62
+# location of data output file
63
+_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/arednsigOutputData.js"
64
+# dummy output data file
65
+_DUMMY_OUTPUT_FILE = _DOCROOT_PATH + "dynamic/nodeOnline.js"
66
+# database that stores node data
67
+_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
68
+
69
+    ### GLOBAL CONSTANTS ###
70
+
71
+# interval in minutes between data requests to the aredn node
72
+_DEFAULT_DATA_REQUEST_INTERVAL = 1
73
+# chart update interval in minutes
74
+_CHART_UPDATE_INTERVAL = 10
75
+
76
+# number seconds to wait for a response to HTTP request
77
+_HTTP_REQUEST_TIMEOUT = 10
78
+# max number of failed data requests allowed
79
+_MAX_FAILED_DATA_REQUESTS = 0
80
+# standard chart width in pixels
81
+_CHART_WIDTH = 600
82
+# standard chart height in pixels
83
+_CHART_HEIGHT = 150
84
+# Set this to True only if this server is intended to relay raw
85
+# node data to a mirror server.
86
+_RELAY_SERVER = False
87
+
88
+   ### GLOBAL VARIABLES ###
89
+
90
+# turn on or off of verbose debugging information
91
+debugOption = False
92
+verboseDebug = False
93
+
94
+# The following two items are used for detecting system faults
95
+# and aredn node online or offline status.
96
+
97
+# count of failed attempts to get data from aredn node
98
+failedUpdateCount = 0
99
+# detected status of aredn node device
100
+nodeOnline = True
101
+
102
+# ip address of aredn node
103
+arednNodeUrl = _DEFAULT_AREDN_NODE_URL
104
+# frequency of data requests to aredn node
105
+dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
106
+# chart update interval
107
+chartUpdateInterval = _CHART_UPDATE_INTERVAL
108
+# last node request time
109
+lastDataPointTime = -1
110
+
111
+  ###  PRIVATE METHODS  ###
112
+
113
+def getTimeStamp():
114
+    """
115
+    Set the error message time stamp to the local system time.
116
+    Parameters: none
117
+    Returns: string containing the time stamp
118
+    """
119
+    return time.strftime( "%m/%d/%Y %T", time.localtime() )
120
+##end def
121
+
122
+def getEpochSeconds(sTime):
123
+    """Convert the time stamp supplied in the weather data string
124
+       to seconds since 1/1/1970 00:00:00.
125
+       Parameters: 
126
+           sTime - the time stamp to be converted must be formatted
127
+                   as %m/%d/%Y %H:%M:%S
128
+       Returns: epoch seconds
129
+    """
130
+    try:
131
+        t_sTime = time.strptime(sTime, '%m/%d/%Y %H:%M:%S')
132
+    except Exception, exError:
133
+        print '%s getEpochSeconds: %s' % (getTimeStamp(), exError)
134
+        return None
135
+    tSeconds = int(time.mktime(t_sTime))
136
+    return tSeconds
137
+##end def
138
+
139
+def setStatusToOffline():
140
+    """Set the detected status of the aredn node to
141
+       "offline" and inform downstream clients by removing input
142
+       and output data files.
143
+       Parameters: none
144
+       Returns: nothing
145
+    """
146
+    global nodeOnline
147
+
148
+    # Inform downstream clients by removing output data file.
149
+    if os.path.exists(_OUTPUT_DATA_FILE):
150
+       os.remove(_OUTPUT_DATA_FILE)
151
+    if os.path.exists(_DUMMY_OUTPUT_FILE):
152
+       os.remove(_DUMMY_OUTPUT_FILE)
153
+    # If the aredn node was previously online, then send
154
+    # a message that we are now offline.
155
+    if nodeOnline:
156
+        print '%s aredn node offline' % getTimeStamp()
157
+    nodeOnline = False
158
+##end def
159
+
160
+def terminateAgentProcess(signal, frame):
161
+    """Send a message to log when the agent process gets killed
162
+       by the operating system.  Inform downstream clients
163
+       by removing input and output data files.
164
+       Parameters:
165
+           signal, frame - dummy parameters
166
+       Returns: nothing
167
+    """
168
+    # Inform downstream clients by removing output data file.
169
+    if os.path.exists(_OUTPUT_DATA_FILE):
170
+       os.remove(_OUTPUT_DATA_FILE)
171
+    if os.path.exists(_DUMMY_OUTPUT_FILE):
172
+       os.remove(_DUMMY_OUTPUT_FILE)
173
+    print '%s terminating arednsig agent process' % \
174
+              (getTimeStamp())
175
+    sys.exit(0)
176
+##end def
177
+
178
+  ###  PUBLIC METHODS  ###
179
+
180
+def getArednNodeData():
181
+    """Send http request to aredn node.  The response from the
182
+       node contains the node signal data as unformatted ascii text.
183
+       Parameters: none
184
+       Returns: a string containing the node signal data if successful,
185
+                or None if not successful
186
+    """
187
+    try:
188
+        conn = urllib2.urlopen(arednNodeUrl, timeout=_HTTP_REQUEST_TIMEOUT)
189
+
190
+        # Format received data into a single string.
191
+        content = ""
192
+        for line in conn:
193
+            content += line.strip()
194
+        del conn
195
+
196
+    except Exception, exError:
197
+        # If no response is received from the device, then assume that
198
+        # the device is down or unavailable over the network.  In
199
+        # that case return None to the calling function.
200
+        print "%s http error: %s" % (getTimeStamp(), exError)
201
+        return None
202
+
203
+    if verboseDebug:
204
+        print "http request successful: %d bytes" % len(content)
205
+
206
+    return content
207
+##end def
208
+
209
+def parseNodeData(sData, dData):
210
+    """Parse the node status page html from the aredn node
211
+       into its component parts.  
212
+       Parameters:
213
+           sData - the string containing the data to be parsed
214
+           dData - a dictionary object to contain the parsed data items
215
+       Returns: True if successful, False otherwise
216
+    """
217
+    try:
218
+
219
+        # Set search boundaries for signal data
220
+        strBeginSearch = '<nobr>Signal/Noise/Ratio</nobr></th>' \
221
+                         '<td valign=middle><nobr><big><b>'
222
+        strEndSearch = 'dB'
223
+        iBeginIndex = sData.find(strBeginSearch) + len(strBeginSearch)
224
+        iEndIndex = sData.find(strEndSearch, iBeginIndex)
225
+
226
+        # Exception if signal data not found
227
+        if iBeginIndex == -1 or iEndIndex == -1:
228
+            raise Exception("signal data not found in status page")
229
+
230
+        # Extract signal data from html
231
+        snr = sData[iBeginIndex:iEndIndex]
232
+        snr = snr.replace(' ','')
233
+        lsnr = snr.split('/')
234
+
235
+        # Store time and signal data in dictionary object
236
+        dData['time'] = getEpochSeconds(getTimeStamp())
237
+        dData['signal'] = lsnr[0]
238
+        dData['noise'] = lsnr[1]
239
+        dData['snr'] = lsnr[2]
240
+    
241
+    except Exception, exError:
242
+        print "%s parse failed: %s" % (getTimeStamp(), exError)
243
+        return False
244
+
245
+    if verboseDebug:
246
+        print "parse successful"
247
+    return True
248
+##end def
249
+
250
+def updateDatabase(dData):
251
+    """
252
+    Update the rrdtool database by executing an rrdtool system command.
253
+    Format the command using the data extracted from the aredn node
254
+    response.   
255
+    Parameters: dData - dictionary object containing data items to be
256
+                        written to the rr database file
257
+    Returns: True if successful, False otherwise
258
+    """
259
+    # Format the rrdtool update command.
260
+    strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s:%s:%s"
261
+    strCmd = strFmt % (_RRD_FILE, dData['time'], dData['signal'], \
262
+             dData['noise'], dData['snr'], '0', \
263
+             '0', '0', '0')
264
+
265
+    if verboseDebug:
266
+        print "%s" % strCmd # DEBUG
267
+
268
+    # Run the command as a subprocess.
269
+    try:
270
+        subprocess.check_output(strCmd, shell=True,  \
271
+                             stderr=subprocess.STDOUT)
272
+    except subprocess.CalledProcessError, exError:
273
+        print "%s: rrdtool update failed: %s" % \
274
+                    (getTimeStamp(), exError.output)
275
+        return False
276
+
277
+    return True
278
+##end def
279
+
280
+def writeOutputDataFile(sData, dData):
281
+    """Write node data items to the output data file, formatted as 
282
+       a Javascript file.  This file may then be accessed and used by
283
+       by downstream clients, for instance, in HTML documents.
284
+       Parameters:
285
+           sData - a string object containing the data to be written
286
+                   to the output data file
287
+       Returns: True if successful, False otherwise
288
+    """
289
+    # Write file for use by html clients.  The following two
290
+    # data items are sent to the client file.
291
+    #    * The last database update date and time
292
+    #    * The data request interval
293
+    lastUpdate = time.strftime( "%m.%d.%Y %T", 
294
+                                time.localtime(dData['time']) )
295
+    sDate = "[{\"date\":\"%s\",\"period\":\"%s\"}]" % \
296
+           (lastUpdate, chartUpdateInterval)
297
+    try:
298
+        fc = open(_DUMMY_OUTPUT_FILE, "w")
299
+        fc.write(sDate)
300
+        fc.close()
301
+    except Exception, exError:
302
+        print "%s write node file failed: %s" % (getTimeStamp(), exError)
303
+        return False
304
+
305
+    if _RELAY_SERVER:
306
+        # Write the entire node data response to the output data file.
307
+        try:
308
+            fc = open(_OUTPUT_DATA_FILE, "w")
309
+            fc.write(sData)
310
+            fc.close()
311
+        except Exception, exError:
312
+            print "%s write output file failed: %s" % \
313
+                  (getTimeStamp(), exError)
314
+            return False
315
+        if verboseDebug:
316
+            print "write output data file: %d bytes" % len(sData)
317
+
318
+    return True
319
+## end def
320
+
321
+def setNodeStatus(updateSuccess):
322
+    """Detect if aredn node is offline or not available on
323
+       the network. After a set number of attempts to get data
324
+       from the node set a flag that the node is offline.
325
+       Parameters:
326
+           updateSuccess - a boolean that is True if data request
327
+                           successful, False otherwise
328
+       Returns: nothing
329
+    """
330
+    global failedUpdateCount, nodeOnline
331
+
332
+    if updateSuccess:
333
+        failedUpdateCount = 0
334
+        # Set status and send a message to the log if the node was
335
+        # previously offline and is now online.
336
+        if not nodeOnline:
337
+            print '%s aredn node online' % getTimeStamp()
338
+            nodeOnline = True
339
+    else:
340
+        # The last attempt failed, so update the failed attempts
341
+        # count.
342
+        failedUpdateCount += 1
343
+
344
+    if failedUpdateCount > _MAX_FAILED_DATA_REQUESTS:
345
+        # Max number of failed data requests, so set
346
+        # node status to offline.
347
+        setStatusToOffline()
348
+##end def
349
+
350
+def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
351
+                lower, upper, addTrend, autoScale):
352
+    """Uses rrdtool to create a graph of specified node data item.
353
+       Parameters:
354
+           fileName - name of file containing the graph
355
+           dataItem - data item to be graphed
356
+           gLabel - string containing a graph label for the data item
357
+           gTitle - string containing a title for the graph
358
+           gStart - beginning time of the graphed data
359
+           lower - lower bound for graph ordinate #NOT USED
360
+           upper - upper bound for graph ordinate #NOT USED
361
+           addTrend - 0, show only graph data
362
+                      1, show only a trend line
363
+                      2, show a trend line and the graph data
364
+           autoScale - if True, then use vertical axis auto scaling
365
+               (lower and upper parameters are ignored), otherwise use
366
+               lower and upper parameters to set vertical axis scale
367
+       Returns: True if successful, False otherwise
368
+    """
369
+    gPath = _CHARTS_DIRECTORY + fileName + ".png"
370
+    trendWindow = { 'end-1day': 7200,
371
+                    'end-4weeks': 172800,
372
+                    'end-12months': 604800 }
373
+ 
374
+    # Format the rrdtool graph command.
375
+
376
+    # Set chart start time, height, and width.
377
+    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
378
+             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
379
+   
380
+    # Set the range and scaling of the chart y-axis.
381
+    if lower < upper:
382
+        strCmd  +=  "-l %s -u %s -r " % (lower, upper)
383
+    elif autoScale:
384
+        strCmd += "-A "
385
+    strCmd += "-Y "
386
+
387
+    # Set the chart ordinate label and chart title. 
388
+    strCmd += "-v %s -t %s " % (gLabel, gTitle)
389
+ 
390
+    # Show the data, or a moving average trend line over
391
+    # the data, or both.
392
+    strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem)
393
+    if addTrend == 0:
394
+        strCmd += "LINE1:dSeries#0400ff "
395
+    elif addTrend == 1:
396
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
397
+                  % trendWindow[gStart]
398
+    elif addTrend == 2:
399
+        strCmd += "LINE1:dSeries#0400ff "
400
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
401
+                  % trendWindow[gStart]
402
+     
403
+    if verboseDebug:
404
+        print "%s" % strCmd # DEBUG
405
+    
406
+    # Run the formatted rrdtool command as a subprocess.
407
+    try:
408
+        result = subprocess.check_output(strCmd, \
409
+                     stderr=subprocess.STDOUT,   \
410
+                     shell=True)
411
+    except subprocess.CalledProcessError, exError:
412
+        print "rrdtool graph failed: %s" % (exError.output)
413
+        return False
414
+
415
+    if debugOption:
416
+        print "rrdtool graph: %s\n" % result,
417
+    return True
418
+
419
+##end def
420
+
421
+def generateGraphs():
422
+    """Generate graphs for display in html documents.
423
+       Parameters: none
424
+       Returns: nothing
425
+    """
426
+    autoScale = False
427
+
428
+    # The following will force creation of charts
429
+    # of only signal strength and S/N charts.  Note that the following
430
+    # data items appear constant and do not show variation with time:
431
+    # noise level, rx mcs, rx rate, tx mcs, tx rate.  Therefore, until
432
+    # these parameters are demonstrated to vary in time, there is no point
433
+    # in creating the charts for these data items.
434
+    createAllCharts = False
435
+
436
+    # 24 hour stock charts
437
+
438
+    createGraph('24hr_signal', 'S', 'dBm', 
439
+                'RSSI\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
440
+    createGraph('24hr_snr', 'SNR', 'dB', 
441
+                'SNR\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
442
+
443
+    # 4 week stock charts
444
+
445
+    createGraph('4wk_signal', 'S', 'dBm', 
446
+                'RSSI\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
447
+    createGraph('4wk_snr', 'SNR', 'dB', 
448
+                'SNR\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
449
+
450
+    # 12 month stock charts
451
+
452
+    createGraph('12m_signal', 'S', 'dBm', 
453
+                'RSSI\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
454
+    createGraph('12m_snr', 'SNR', 'dB', 
455
+                'SNR\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
456
+
457
+    if debugOption:
458
+        #print # print a blank line to improve readability when in debug mode
459
+        pass
460
+##end def
461
+
462
+def getCLarguments():
463
+    """Get command line arguments.  There are four possible arguments
464
+          -d turns on debug mode
465
+          -v turns on verbose debug mode
466
+          -t sets the aredn node query interval
467
+          -u sets the url of the aredn nodeing device
468
+       Returns: nothing
469
+    """
470
+    global debugOption, verboseDebug, dataRequestInterval, \
471
+           arednNodeUrl
472
+
473
+    index = 1
474
+    while index < len(sys.argv):
475
+        if sys.argv[index] == '-d':
476
+            debugOption = True
477
+        elif sys.argv[index] == '-v':
478
+            debugOption = True
479
+            verboseDebug = True
480
+        elif sys.argv[index] == '-p':
481
+            try:
482
+                dataRequestInterval = abs(int(sys.argv[index + 1]))
483
+            except:
484
+                print "invalid polling period"
485
+                exit(-1)
486
+            index += 1
487
+        elif sys.argv[index] == '-u':
488
+            arednNodeUrl = sys.argv[index + 1]
489
+            index += 1
490
+        else:
491
+            cmd_name = sys.argv[0].split('/')
492
+            print "Usage: %s [-d] [-v] [-p seconds] [-u url]" % cmd_name[-1]
493
+            exit(-1)
494
+        index += 1
495
+##end def
496
+
497
+def main():
498
+    """Handles timing of events and acts as executive routine managing
499
+       all other functions.
500
+       Parameters: none
501
+       Returns: nothing
502
+    """
503
+    global dataRequestInterval
504
+
505
+    signal.signal(signal.SIGTERM, terminateAgentProcess)
506
+
507
+    print '%s starting up arednsig agent process' % \
508
+                  (getTimeStamp())
509
+
510
+    # last time output JSON file updated
511
+    lastDataRequestTime = -1
512
+    # last time charts generated
513
+    lastChartUpdateTime = - 1
514
+
515
+    ## Get command line arguments.
516
+    getCLarguments()
517
+
518
+    requestIntervalSeconds = dataRequestInterval * 60 # convert to seconds
519
+    chartUpdateIntervalSeconds = chartUpdateInterval * 60 # charts interval
520
+
521
+    ## Exit with error if rrdtool database does not exist.
522
+    if not os.path.exists(_RRD_FILE):
523
+        print 'rrdtool database does not exist\n' \
524
+              'use createArednsigRrd script to ' \
525
+              'create rrdtool database\n'
526
+        exit(1)
527
+ 
528
+    ## main loop
529
+    while True:
530
+
531
+        currentTime = time.time() # get current time in seconds
532
+
533
+        # Every web update interval request data from the aredn
534
+        # node and process the received data.
535
+        if currentTime - lastDataRequestTime > requestIntervalSeconds:
536
+            lastDataRequestTime = currentTime
537
+            dData = {}
538
+            result = True
539
+
540
+            # Get the data string from the device.
541
+            sData = getArednNodeData()
542
+            # If the first http request fails, try one more time.
543
+            if sData == None:
544
+                result = False
545
+
546
+            # If successful parse the data.
547
+            if result:
548
+                result = parseNodeData(sData, dData)
549
+           
550
+            # If parse successful, write data to data files.
551
+            if result:
552
+                result = updateDatabase(dData)
553
+
554
+            if result:
555
+                writeOutputDataFile(sData, dData)
556
+
557
+            # Set the node status to online or offline depending on the
558
+            # success or failure of the above operations.
559
+            setNodeStatus(result)
560
+
561
+        # At the chart generation interval, generate charts.
562
+        if currentTime - lastChartUpdateTime > chartUpdateIntervalSeconds:
563
+            lastChartUpdateTime = currentTime
564
+            p = multiprocessing.Process(target=generateGraphs, args=())
565
+            p.start()
566
+
567
+        # Relinquish processing back to the operating system until
568
+        # the next update interval.
569
+
570
+        elapsedTime = time.time() - currentTime
571
+        if debugOption:
572
+            if result:
573
+                print "%s update successful:" % getTimeStamp(),
574
+            else:
575
+                print "%s update failed:" % getTimeStamp(),
576
+            print "%6f seconds processing time\n" % elapsedTime 
577
+        remainingTime = requestIntervalSeconds - elapsedTime
578
+        if remainingTime > 0.0:
579
+            time.sleep(remainingTime)
580
+    ## end while
581
+    return
582
+## end def
583
+
584
+if __name__ == '__main__':
585
+    try:
586
+        main()
587
+    except KeyboardInterrupt:
588
+        print '\n',
589
+        terminateAgentProcess('KeyboardInterrupt','Module')