# HG changeset patch # User Daniel O'Connor # Date 1463722603 -34200 # Node ID 2debc3fb437232d4b28238572cd1415e22b78237 # Parent bf46efd061d793a9fc48c6f47ecb5afff1247495 Update for VDSL modem (TP-Link W9970). Log maximum as well as current rates. Remove trailing whitespace. Keep 5 years of data. diff -r bf46efd061d7 -r 2debc3fb4372 adslstats.py --- a/adslstats.py Thu Jan 08 11:28:28 2015 +1030 +++ b/adslstats.py Fri May 20 15:06:43 2016 +0930 @@ -1,7 +1,7 @@ #!/usr/bin/env python2 ############################################################################ # -# Parse ADSL link stats for Billion 7300G & generate RRD archives & graphs +# Parse ADSL link stats for TP-Link W9970 & generate RRD archives & graphs # ############################################################################ # @@ -30,10 +30,12 @@ # ############################################################################ +import base64 import ConfigParser import optparse import os import re +import requests import rrdtool import sys import time @@ -44,7 +46,7 @@ conf.add_section('global') conf.set('global', 'username', 'admin') conf.set('global', 'password', 'admin') -conf.set('global', 'name', 'dsl.dons.net.au') +conf.set('global', 'name', '10.0.2.13') conflist = ['adslstats.ini'] if ('HOME' in os.environ): @@ -86,60 +88,71 @@ class ADSLStats(object): def __str__(self): - return """Line Rate - Up: %d kbits, Down %d kbits -Noise Margin - Up: %.1f dB, Down %.1f dB -Attenuation - Up: %.1f dB, Down %.1f dB""" % (self.upstream, self.downstream, - self.nmup, self.nmdown, + s = "Line Rate - Up: %d kbits, Down %d kbits\n" % (self.upstream, self.downstream) + if hasattr(self, 'upstreammax'): + s += "Maximum Rate - Up: %d kbit, Down %s kbit\n" % (self.upstreammax, self.downstreammax) + s += """Noise Margin - Up: %.1f dB, Down %.1f dB +Attenuation - Up: %.1f dB, Down %.1f dB""" % (self.nmup, self.nmdown, self.attenup, self.attendown) -def getstats(f): - s = BeautifulSoup(f) - a = s.findAll('tr') - - for i in statsdict: - assert a[i].td.contents[0] == statsdict[i] + return s +def getstats(): stats = ADSLStats() - - # Check if the modem is offline - if a[3].td.findNext('td').contents[0] != 'Up': - return None - - # dB - stats.nmdown = float(a[8].td.findNext('td').contents[0]) / 10.0 - stats.nmup = float(a[8].td.findNext('td').findNext('td').contents[0]) / 10.0 - stats.attendown = float(a[9].td.findNext('td').contents[0]) / 10.0 - stats.attenup = float(a[9].td.findNext('td').findNext('td').contents[0]) / 10.0 - # kBit - stats.downstream = float(a[14].td.findNext('td').contents[0]) - stats.upstream = float(a[14].td.findNext('td').findNext('td').contents[0]) + parser = ConfigParser.ConfigParser() + base = 'http://%s' % (conf.get('global', 'name')) + # Gunk extracted from Chrome (what the page is requesting). Note it's sensitive to line ending type... + # We could get more data, eg error rates.. + data = '[WAN_DSL_INTF_CFG#1,0,0,0,0,0#0,0,0,0,0,0]0,12\r\nstatus\r\nmodulationType\r\nX_TP_AdslModulationCfg\r\nupstreamCurrRate\r\ndownstreamCurrRate\r\nX_TP_AnnexType\r\nupstreamMaxRate\r\ndownstreamMaxRate\r\nupstreamNoiseMargin\r\ndownstreamNoiseMargin\r\nupstreamAttenuation\r\ndownstreamAttenuation\r\n[WAN_DSL_INTF_STATS_TOTAL#1,0,0,0,0,0#0,0,0,0,0,0]1,8\r\nATUCCRCErrors\r\nCRCErrors\r\nATUCFECErrors\r\nFECErrors\r\nSeverelyErroredSecs\r\nX_TP_US_SeverelyErroredSecs\r\nerroredSecs\r\nX_TP_US_ErroredSecs\r\n' + cookies = {'Authorization' : 'Basic ' + base64.standard_b64encode(conf.get('global', 'username') + ':' + conf.get('global', 'password'))} + headers = {'Referer' : base} + r = requests.post(base + '/cgi?1&5' , data = data, headers = headers, cookies = cookies, stream = True) + parser.readfp(r.raw) + res = {} + tmp = '1,0,0,0,0,0' + if parser.get(tmp, 'status') == 'Up': + stats.linkup = True + else: + stats.linkup = False + stats.upstream = float(parser.get(tmp, 'upstreamCurrRate')) + stats.downstream = float(parser.get(tmp, 'downstreamCurrRate')) + stats.upstreammax = float(parser.get(tmp, 'upstreamMaxRate')) + stats.downstreammax = float(parser.get(tmp, 'downstreamMaxRate')) + stats.nmup = float(parser.get(tmp, 'upstreamNoiseMargin')) / 10.0 + stats.nmdown = float(parser.get(tmp, 'downstreamNoiseMargin')) / 10.0 + stats.attenup = float(parser.get(tmp, 'upstreamAttenuation')) / 10.0 + stats.attendown = float(parser.get(tmp, 'downstreamAttenuation')) / 10.0 return stats # Setup RRD # We expect data to be logged every 5 minutes # Average 12 5 minute points -> hourly stats (keep 168 - a weeks worth) -# Average 288 5 minute points -> daily stats (keep 365 - a years worth) +# Average 288 5 minute points -> daily stats (keep 1825 - 5 years worth) # Detemine minimum & maximum for an hour and keep a weeks worth. def makerrd(filename): rrdtool.create(filename, '--step', '300', - 'DS:upstream:GAUGE:3600:32:25000', # Upstream (kbits) - 24mbit is ADSL2+ max - 'DS:downstream:GAUGE:3600:32:25000', # Downstream (kbits) + 'DS:upstream:GAUGE:3600:32:150000', # Upstream (kbits) + 'DS:downstream:GAUGE:3600:32:150000', # Downstream (kbits) + 'DS:upstreammax:GAUGE:3600:32:150000', # Upstream maximum (kbits) + 'DS:downstreammax:GAUGE:3600:32:150000', # Downstream maximum (kbits) 'DS:nmup:GAUGE:3600:0:100', # Upstream Noise margin (dB) 'DS:nmdown:GAUGE:3600:0:100', # Downstream Noise margin (dB) 'DS:attenup:GAUGE:3600:0:100', # Upstream Attenuation (dB) 'DS:attendown:GAUGE:3600:0:100', # Downstream Attenuation (dB) 'RRA:AVERAGE:0.1:12:168', - 'RRA:AVERAGE:0.1:288:365', + 'RRA:AVERAGE:0.1:288:1825', 'RRA:MIN:0.1:12:168', 'RRA:MAX:0.1:12:168') # Update the RRD (format stats as expected) def updaterrd(filename, tstamp, stats): rrdtool.update(filename, - '%d:%d:%d:%f:%f:%f:%f' % (tstamp, + '%d:%d:%d:%d:%d:%f:%f:%f:%f' % (tstamp, stats.upstream, stats.downstream, + stats.upstreammax, + stats.downstreammax, stats.nmup, stats.nmdown, stats.attenup, @@ -147,13 +160,7 @@ # Open the URL and call the parser def getdata(): - opener = urllib.FancyURLopener() - opener.prompt_user_passwd = lambda host, realm: (options.authname, options.password) - f = opener.open(statsurl) - #f = open("adsl.html") - stats = getstats(f) - if stats == None: - return None + stats = getstats() return stats # Generate a graph @@ -162,27 +169,35 @@ linkargs = ( '-a', 'SVG', '-X', '0', + '-l', '0', '--vertical-label', 'kbit/sec', '--slope-mode', 'DEF:upstream=%s:upstream:AVERAGE' % rrdname, 'DEF:upstreammin=%s:upstream:MIN' % rrdname, 'DEF:upstreammax=%s:upstream:MAX' % rrdname, - + 'DEF:downstream=%s:downstream:AVERAGE' % rrdname, 'DEF:downstreammin=%s:downstream:MIN' % rrdname, 'DEF:downstreammax=%s:downstream:MAX' % rrdname, 'CDEF:upstreamdif=upstreammax,upstreammin,-', 'CDEF:downstreamdif=downstreammax,downstreammin,-', - + + 'DEF:maxupstream=%s:upstreammax:AVERAGE' % rrdname, + 'DEF:maxdownstream=%s:downstreammax:AVERAGE' % rrdname, + 'LINE0:upstreammin#000000:', 'AREA:upstreamdif#00dc76::STACK', 'LINE1:upstream#00ff00:Upstream', - + 'LINE0:downstreammin#000000:', 'AREA:downstreamdif#ff8686::STACK', - 'LINE1:downstream#ff0000:Downstream') + 'LINE1:downstream#ff0000:Downstream', + + 'LINE1:maxupstream#0000ff:Upstream (maximum)', + 'LINE1:maxdownstream#000000:Downstream (maximum)' + ) signalargs = ( '-a', 'SVG', @@ -191,53 +206,53 @@ 'DEF:upstream=%s:upstream:AVERAGE' % rrdname, 'DEF:downstream=%s:downstream:AVERAGE' % rrdname, - + 'DEF:nmup_=%s:nmup:AVERAGE' % rrdname, 'DEF:nmupmin_=%s:nmup:MIN' % rrdname, 'DEF:nmupmax_=%s:nmup:MAX' % rrdname, - + 'DEF:nmdown_=%s:nmdown:AVERAGE' % rrdname, 'DEF:nmdownmin_=%s:nmdown:MIN' % rrdname, 'DEF:nmdownmax_=%s:nmdown:MAX' % rrdname, - + 'DEF:attenup=%s:attenup:AVERAGE' % rrdname, 'DEF:attenupmin=%s:attenup:MIN' % rrdname, 'DEF:attenupmax=%s:attenup:MAX' % rrdname, - + 'DEF:attendown=%s:attendown:AVERAGE' % rrdname, 'DEF:attendownmin=%s:attendown:MIN' % rrdname, 'DEF:attendownmax=%s:attendown:MAX' % rrdname, - + 'CDEF:nmup=nmup_,10,*', 'CDEF:nmupmin=nmupmin_,10,*', 'CDEF:nmupmax=nmupmax_,10,*', 'CDEF:nmupdif=nmupmax,nmupmin,-', - + 'CDEF:nmdown=nmdown_,10,*', 'CDEF:nmdownmin=nmdownmin_,10,*', 'CDEF:nmdownmax=nmdownmax_,10,*', 'CDEF:nmdowndif=nmdownmax,nmdownmin,-', - + 'CDEF:attenupdif=attenupmax,attenupmin,-', - + 'CDEF:attendowndif=attendownmax,attendownmin,-', - + 'LINE0:nmupmin#000000:', 'AREA:nmupdif#5c5cff::STACK', 'LINE1:nmup#0000ff:Noise Margin - Up (1/10 dB)', - + 'LINE0:nmdownmin#000000:', 'AREA:nmdowndif#009a00::STACK', 'LINE1:nmdown#00ff00:Noise Margin - Down (1/10 dB)', - + 'LINE0:attenupmin#000000:', 'AREA:attenupdif#f98100::STACK', 'LINE1:attenup#ff0000:Attenuation - Up', - + 'LINE0:attendownmin#000000:', 'AREA:attendowndif#aaaaaa::STACK', 'LINE1:attendown#000000:Attenuation - Down') - + rrdtool.graph("%s-hour-link.svg" % (graphbasename), '--width', '768', '--height', '256', @@ -306,10 +321,11 @@ sys.exit(0) if options.update or options.munin: stats = getdata() - if stats == None: - if options.verbose: + if options.verbose: + if stats == None: print "Modem is offline" - + else: + print stats if (options.update or options.munin != None) and stats != None: if options.update: try: