#!/usr/bin/python
"""
Last updated: 3-15-2010

Script to poll ZyXEL P-600 Series DSL modem for diagnostics. Deals
with 2 difficulties in gathering diagnostics:
1) web interface requires authentication
2) refresh of diagnostics on page uses javascript

Written for this device specifically, yours may or may not work.

P-660R-ELNK
ZyNOS F/W Version: V3.40(AHC.0) | 11/16/2005
DSL FW Version:TI AR7 05.00.03.e6

"""

import datetime, httplib, re, sys, time

host = '192.168.1.1'

# in seconds
poll_interval = 30 

# disguise as FireFox or modem refuses to serve page
headers = { "User-Agent" : "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7" }

noiseRE = re.compile(r'noise margin downstream: (\d+) db')
outputPowerRE = re.compile(r'output power upstream: (\d+) db')
attenuationRE = re.compile(r'attenuation downstream: (\d+) db')

def now_str():
    return datetime.datetime.today().ctime()

def log(msg):
    print now_str() + " " + msg
    sys.stdout.flush()

# Values are from http://www.dslreports.com/faq/6734

noise_dict = { 1000 : "outstanding", 29 : "excellent", 20 : "good", 11 : "fair", 6 : "bad" }

def lookup(value, dict):
    keys = dict.keys()
    keys.sort()
    for threshold in keys:
        if value <= threshold:
            return dict[threshold]
    
def noise_as_str(value):
    return lookup(value, noise_dict)

attenuation_dict = { 1000 : "bad", 60 : "poor", 50 : "good", 40 : "very good", 30 : "excellent", 20 : "outstanding" }

def attenuation_as_str(value):
    return lookup(value, attenuation_dict)

class HTTPAction:
    """ base class for wrapping an HTTP connection to DSL modem """

    def __init__(self):
        self.errormsg = None
        self.conn = None

    def do_work(self):
        """ should be implemented by subclasses and return True if
        successful, False otherwise. do_work should call do_http and
        do any parsing """
        return True

    def do_http(self, method, path, params="", headers=None):
        try:
            self.conn = httplib.HTTPConnection(host)
            if headers is None:
                self.conn.request(method, path, params)
            else:
                self.conn.request(method, path, params, headers)
            self.response = self.conn.getresponse()
        except Exception, e:
            self.error("Couldn't establish connection to DSL modem")

    def error(self, msg):
        self.errormsg = msg
        return False

    def geterrormsg(self):
        return self.errormsg

    def close(self):
        if self.conn is not None:
            self.conn.close()        


class Login(HTTPAction):
    """ Logs into the ZyXEL device, which seems to only permit one IP
    to use the web admin interface as a time. it must track the IP
    internally rather than use cookies """

    def do_work(self):
        # assumes default password
        self.do_http("POST", "/Forms/rpAuth_1", "LoginPassword=ZyXEL+ZyWALL+Series&hiddenPassword=81dc9bdb52d04dc20036dbd8313ed055&Prestige_Login=Login")
        if self.geterrormsg() is not None:
            return False
        if self.response.status != 303:
            return self.error("expected 303, got " + str(self.response.status) + " instead")
        return True


class RefreshStats(HTTPAction):
    """ Posts the form for refreshing the stat page. this results in a
    redirect to a page we fetch in StatsPage """

    def do_work(self):
        self.do_http("POST", "/Forms/DiagADSL_1", "LineInfoDisplay=&DiagDownstreamNoise=Downstream+Noise+Margin", headers)
        if self.geterrormsg() is not None:
            return False
        if self.response.status != 303:
            return self.error("expected 303, got " + str(self.response.status) + " instead")
        redirectURL = self.response.getheader("Location")
        if redirectURL != "http://192.168.1.1/DiagADSL.html":
            return self.error("Expected redirect to http://192.168.1.1/DiagADSL.html," + \
                             " got " + redirectURL + " instead.")
        return True

    
class StatsPage(HTTPAction):

    def __init__(self):
        self.noise = None
        self.outputPower = None
        self.attenuation = None
        HTTPAction.__init__(self)

    def do_work(self):
        self.do_http("GET", "/DiagADSL.html", "", headers) 

        if self.geterrormsg() is not None:
            return False

        try:
            data = self.response.read()
        except Exception, e:
            return self.error("error reading data stream")

        self.noise = self.parse(noiseRE, data)
        self.outputPower = self.parse(outputPowerRE, data)
        self.attenuation = self.parse(attenuationRE, data)

        if self.noise is None or self.outputPower is None or self.attenuation is None:
            return False
        return True

    def parse(self, re, data):
        match = re.search(data)
        if match is not None:
            return int(match.group(1))
        self.error("Could not parse value")
        return None

    def getstats(self):
        return "noise = " + str(self.noise) \
            + " (" + noise_as_str(self.noise) + ")" \
            + " outputPower = " + str(self.outputPower) \
            + " attenuation = " + str(self.attenuation) \
            + " (" + attenuation_as_str(self.attenuation) + ")" \


def fetch_diagnostic():
    """ performs a single sequence of HTTP requests to fetch
    diagnostics and prints out a line of output """

    # Login
    login = Login()
    if login.do_work():

        # Simulate clicking on button to refresh stats display
        refreshStats = RefreshStats()
        if refreshStats.do_work():

            # Grab the page which should have data in it
            statsPage = StatsPage()
            if statsPage.do_work():
                log(statsPage.getstats())
            else:
                log("page failed: " + statsPage.geterrormsg())
            statsPage.close()
        else:
            log("refresh stats failed: " + refreshStats.geterrormsg())
        refreshStats.close()
    else:
        log("login failed: " + login.geterrormsg())
    login.close()

#### main ####

if __name__ == "__main__":
    while True:
        fetch_diagnostic()
        time.sleep(poll_interval)
