changeset 980:2f1cb8b5950a

rewrites for better graph export, removal of dhcp reader Ignore-this: ecc5280b15d66020412f82ad84862074 darcs-hash:20150504002120-312f9-85bcb342f5bdae78d7b6d9083929944d4a467001
author drewp <drewp@bigasterisk.com>
date Sun, 03 May 2015 17:21:20 -0700
parents 76bb0bf74bd1
children d9bbbd8d86f6
files service/tomatoWifi/dhcpparse.py service/tomatoWifi/index.html service/tomatoWifi/tomatoWifi.py service/tomatoWifi/wifi.py
diffstat 4 files changed, 182 insertions(+), 81 deletions(-) [+]
line wrap: on
line diff
--- a/service/tomatoWifi/dhcpparse.py	Sat May 02 18:52:15 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-def addDhcpData(rows):
-    """given dicts with 'mac', add other data from the dhcp cache"""
-    currentLease = None
-    for line in open('/var/lib/dhcp/dhcpd.leases'):
-        if line.startswith('lease '):
-            currentLease = {'ip': line.split()[1]}
-        elif line.startswith('  hardware ethernet '):
-            currentLease['mac'] = line.split()[2].strip(';').upper()
-        elif line.startswith('  client-hostname'):
-            currentLease['clientHostname'] = line.split(None, 2)[1].strip('";')
-        elif line.startswith('  binding state'):
-            currentLease['bindingState'] = line.split()[2].strip(';')
-        elif line.startswith('}'):
-            if currentLease.get('bindingState') == 'active':
-                # there seem to be a lot of 'active' blocks
-                # for the same ip addr and mac. I haven't
-                # looked into whether they hold different
-                # lease terms or what
-                for r in rows:
-                    if r['mac'] == currentLease['mac']:
-                        r.update(currentLease)
-                        
-            
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/tomatoWifi/index.html	Sun May 03 17:21:20 2015 -0700
@@ -0,0 +1,79 @@
+<!doctype html>
+<html>
+  <head>
+    <title>dhcp leases</title>
+    <meta charset="utf-8" />
+    <script src="/lib/polymer/0.8/webcomponentsjs/webcomponents-lite.min.js"></script>
+    <script src="/lib/underscore-1.5.2.min.js"></script>
+    <link rel="import" href="/rdf/n3+polymer/trig-store.html">
+    <link rel="import" href="/lib/polymer/0.8/iron-ajax/iron-ajax.html">
+  </head>
+  <body>
+    <h1>Devices on wifi</h1>
+
+    <dom-module id="wifi-table">
+      <template>
+        <iron-ajax auto url="graph"
+                   handle-as="text"
+                   last-response="{{ajaxResponse}}"></iron-ajax>
+        <trig-store id="ts" trig-input="{{ajaxResponse}}"></trig-store>
+
+        <table>
+          <tr>
+            <th>name</th>
+            <th>MAC</th>
+            <th>Connected</th>
+          </tr>
+          <template is="x-repeat" items="{{devices}}">
+            <tr>
+              <td>{{item.deviceName}}</td>
+              <td>{{item.mac}}</td>
+              <td>{{item.connectedAgo}}</td>
+            </tr>
+          </template>
+        </table>
+      </template>
+    </dom-module>
+    
+    <script>
+     Polymer({
+       is: "wifi-table",
+       ready: function() {
+         this.$.ts.addEventListener('store-changed', this.storeChanged.bind(this));
+         this.devices = [];
+       },
+       storeChanged: function(ev) {
+         var store = ev.detail.value;
+         var find = function(s, p, o) { return store.findAllGraphs(s, p, o); };
+         var findOne = function(s, p, o) {
+           var rows = find(s, p, o);
+           return rows[0];
+         };
+
+         this.devices = [];
+         
+         find(null, "room:connected", "http://bigasterisk.com/wifiAccessPoints"
+              ).forEach(function(row) {
+           var out = {
+             mac: N3.Util.getLiteralValue(
+               findOne(row.subject, "room:macAddress", null).object),
+             connectedAgo: N3.Util.getLiteralValue(
+               findOne(row.subject, "room:connectedAgo", null).object)
+            
+           };
+           try {
+             var dev = findOne(row.subject, "room:deviceName", null).object;
+             out.deviceName = N3.Util.getLiteralValue(dev);
+           } catch(e) {
+
+           }           
+           
+           this.devices.push(out);
+         }.bind(this));
+         this.devices = _.sortBy(this.devices, 'deviceName');
+       }
+     });
+    </script>
+    <wifi-table></wifi-table>
+  </body>
+</html>
--- a/service/tomatoWifi/tomatoWifi.py	Sat May 02 18:52:15 2015 -0700
+++ b/service/tomatoWifi/tomatoWifi.py	Sun May 03 17:21:20 2015 -0700
@@ -25,20 +25,11 @@
 sys.path.append("/my/site/magma")
 from stategraph import StateGraph
 from wifi import Wifi
-from dhcpparse import addDhcpData
 
 sys.path.append("/my/proj/homeauto/lib")
 from cycloneerr import PrettyErrorHandler
 from logsetup import log
 
-import rdflib
-from rdflib import plugin
-plugin.register(
-  "sparql", rdflib.query.Processor,
-  "rdfextras.sparql.processor", "Processor")
-plugin.register(
-  "sparql", rdflib.query.Result,
-  "rdfextras.sparql.query", "SPARQLQueryResult")
 
 DEV = Namespace("http://projects.bigasterisk.com/device/")
 ROOM = Namespace("http://projects.bigasterisk.com/room/")
@@ -50,9 +41,28 @@
         age = time.time() - self.settings.poller.lastPollTime
         if age > 10:
             raise ValueError("poll data is stale. age=%s" % age)
+            
+        self.set_header("Content-Type", "text/html")
+        self.write(open("index.html").read())
 
-        self.write("this is wifiusage. needs index page that embeds the table")
+def whenConnected(mongo, macThatIsNowConnected):
+    lastArrive = None
+    for ev in mongo.find({'address': macThatIsNowConnected.upper()},
+                         sort=[('created', -1)],
+                         max_scan=100000):
+        if ev['action'] == 'arrive':
+            lastArrive = ev
+        if ev['action'] == 'leave':
+            break
+    if lastArrive is None:
+        raise ValueError("no past arrivals")
 
+    return lastArrive['created']
+
+def connectedAgoString(conn):
+    return web.utils.datestr(
+        conn.astimezone(tz.tzutc()).replace(tzinfo=None))
+    
 class Table(PrettyErrorHandler, cyclone.web.RequestHandler):
     def get(self):
         def rowDict(row):
@@ -60,15 +70,14 @@
             if 'name' not in row:
                 row['name'] = row.get('clientHostname', '-')
             if 'signal' not in row:
-                row['signal'] = 'yes' if row['connected'] else 'no'
+                row['signal'] = 'yes' if row.get('connected') else 'no'
 
             try:
-                conn = self.whenConnected(row['mac'])
-                row['connectedAgo'] = web.utils.datestr(
-                    conn.astimezone(tz.tzutc()).replace(tzinfo=None))
+                conn = whenConnected(self.settings.mongo, row.get('mac', '??'))
+                row['connectedAgo'] = connectedAgoString(conn)
             except ValueError:
-                pass
-            
+                row['connectedAgo'] = 'yes' if row.get('connected') else ''
+            row['router'] = row.get('ssid', '')
             return row
 
         self.set_header("Content-Type", "application/xhtml+xml")
@@ -78,21 +87,7 @@
                 rows=sorted(map(rowDict, self.settings.poller.lastAddrs),
                             key=lambda a: (not a.get('connected'),
                                            a.get('name'))))))
-
-    def whenConnected(self, macThatIsNowConnected):
-        lastArrive = None
-        for ev in self.settings.mongo.find({'address': macThatIsNowConnected},
-                                           sort=[('created', -1)],
-                                           max_scan=100000):
-            if ev['action'] == 'arrive':
-                lastArrive = ev
-            if ev['action'] == 'leave':
-                break
-        if lastArrive is None:
-            raise ValueError("no past arrivals")
-        
-        return lastArrive['created']
-        
+       
 
 class Json(PrettyErrorHandler, cyclone.web.RequestHandler):
     def get(self):
@@ -128,6 +123,14 @@
                 g.add((uri, ROOM['deviceName'], Literal(dev['name'])))
             if 'signal' in dev:
                 g.add((uri, ROOM['signalStrength'], Literal(dev['signal'])))
+            try:
+                conn = whenConnected(self.settings.mongo, dev['mac'])
+            except ValueError:
+                pass
+            else:
+                g.add((uri, ROOM['connectedAgo'],
+                       Literal(connectedAgoString(conn))))
+                g.add((uri, ROOM['connected'], Literal(conn)))
 
         self.set_header('Content-type', 'application/x-trig')
         self.write(g.asTrig())
@@ -146,6 +149,12 @@
 
     @inlineCallbacks
     def poll(self):
+
+        connectedField = 'connected'
+        
+        # UVA mode:
+        addDhcpData = lambda *args: None
+        
         try:
             newAddrs = yield self.wifi.getPresentMacAddrs()
             addDhcpData(newAddrs)
@@ -170,17 +179,17 @@
             self.lastAddrs = newAddrs
             self.lastPollTime = time.time()
         except Exception, e:
-            log.error("poll error: %s\n%s", e, traceback.format_exc())
+            log.error("poll error: %r\n%s", e, traceback.format_exc())
 
     def computeActions(self, newWithSignal):
         actions = []
 
         def makeAction(addr, act):
             d = dict(sensor="wifi",
-                        address=addr.get('mac'),
-                        name=addr.get('name'),
-                        networkName=addr.get('clientHostname'),
-                        action=act)
+                     address=addr.get('mac').upper(), # mongo data is legacy uppercase
+                     name=addr.get('name'),
+                     networkName=addr.get('clientHostname'),
+                     action=act)
             if act == 'arrive' and 'ip' in addr:
                 # this won't cover the possible case that you get on
                 # wifi but don't have an ip yet. We'll record an
@@ -231,7 +240,7 @@
         'pollFrequency' : 1/5,
         }
     from twisted.python import log as twlog
-    #log.startLogging(sys.stdout)
+    #twlog.startLogging(sys.stdout)
     #log.setLevel(10)
     #log.setLevel(logging.DEBUG)
 
--- a/service/tomatoWifi/wifi.py	Sat May 02 18:52:15 2015 -0700
+++ b/service/tomatoWifi/wifi.py	Sun May 03 17:21:20 2015 -0700
@@ -1,8 +1,8 @@
-import re, ast, logging, socket
+import re, ast, logging, socket, json
 import lxml.html.soupparser
 from twisted.internet.defer import inlineCallbacks, returnValue
 from cyclone.httpclient import fetch
-from rdflib import Literal, Graph
+from rdflib import Literal, Graph, RDFS, URIRef
 
 log = logging.getLogger()
 
@@ -13,32 +13,31 @@
 class Wifi(object):
     """
     gather the users of wifi from the tomato routers
-
-    with host names from /var/lib/dhcp/dhcpd.leases
     """
-    def __init__(self, tomatoConfig="/my/site/magma/tomato_config.js",
-                 accessN3="/my/proj/openid_proxy/access.n3"):
+    def __init__(self, accessN3="/my/proj/openid_proxy/access.n3"):
+        self.graph = Graph()
+        self.graph.parse('config.n3', format='n3')
 
-        # ideally this would all be in the same rdf store, with int and
-        # ext versions of urls
-
-        txt = open(tomatoConfig).read().replace('\n', '')
-        self.knownMacAddr = jsValue(txt, 'knownMacAddr')
-        tomatoUrl = jsValue(txt, 'tomatoUrl')
-
+        #self._loadRouters(accessN3, tomatoUrl)
+        
+    def _loadRouters(self, accessN3, tomatoUrl):
         g = Graph()
         g.parse(accessN3, format="n3")
-        repl = {'/tomato1/' : None, '/tomato2/' : None}
+        repl = {
+            '/wifiRouter1/' : None,
+            #'/tomato2/' : None
+        }
         for k in repl:
             rows = list(g.query('''
             PREFIX p: <http://bigasterisk.com/openid_proxy#>
             SELECT ?prefix WHERE {
-              [
+              ?site
                 p:requestPrefix ?public;
                 p:proxyUrlPrefix ?prefix
-                ]
+                .
             }''', initBindings={"public" : Literal(k)}))
             repl[k] = str(rows[0][0])
+        log.debug('repl %r', repl)
 
         self.routers = []
         for url in tomatoUrl:
@@ -51,12 +50,24 @@
 	    userPass, tail = tail.split("@", 1)
             r.url = http + '//' + tail
             r.headers = {'Authorization': ['Basic %s' % userPass.encode('base64').strip()]}
-            r.name = {'tomato1' : 'bigasterisk5',
+            r.name = {'wifiRouter1' : 'bigasterisk5',
                       'tomato2' : 'bigasterisk4'}[name.split('/')[1]]
             self.routers.append(r)
 
     @inlineCallbacks
     def getPresentMacAddrs(self):
+        rows = yield loadUvaData()
+        for row in rows:
+            if 'clientHostname' in row:
+                row['name'] = row['clientHostname']
+            mac = URIRef('http://bigasterisk.com/mac/%s' % row['mac'].lower())
+            label = self.graph.value(mac, RDFS.label)
+            if label:
+                row['name'] = label
+        returnValue(rows)
+            
+    @inlineCallbacks
+    def getPresentMacAddrs_multirouter(self):
         rows = []
         
         for router in self.routers:
@@ -70,10 +81,10 @@
             data = resp.body
             if 'Wireless -- Authenticated Stations' in data:
                 # zyxel 'Station Info' page
-                rows.extend(self.parseZyxel(data, router.name))
+                rows.extend(self._parseZyxel(data, router.name))
             else:
                 # tomato page
-                rows.extend(self.parseTomato(data, router.name))
+                rows.extend(self._parseTomato(data, router.name))
 
         for r in rows:
             try:
@@ -83,7 +94,7 @@
                 
         returnValue(rows)
         
-    def parseZyxel(self, data, routerName):
+    def _parseZyxel(self, data, routerName):
         root = lxml.html.soupparser.fromstring(data)
         for tr in root.cssselect('tr'):
             mac, assoc, uth, ssid, iface = [td.text_content().strip() for td in tr.getchildren()]
@@ -92,10 +103,35 @@
             assoc = assoc.lower() == 'yes'
             yield dict(router=routerName, mac=mac, assoc=assoc, connected=assoc)
 
-    def parseTomato(self, data, routerName):
+    def _parseTomato(self, data, routerName):
         for iface, mac, signal in jsValue(data, 'wldev'):
             yield dict(router=routerName, mac=mac, signal=signal, connected=bool(signal))
+
+
+@inlineCallbacks
+def loadUvaData():
+    config = json.load(open("/my/proj/homeauto/service/tomatoWifi/priv-uva.json"))
+    headers = {'Authorization': ['Basic %s' % config['userPass'].encode('base64').strip()]}
+    resp = yield fetch('http://10.2.0.2/wlstationlist.cmd', headers=headers)
+    root = lxml.html.soupparser.fromstring(resp.body)
+    byMac = {}
+    for tr in root.cssselect('tr'):
+        mac, connected, auth, ssid, iface = [td.text_content().strip() for td in tr.getchildren()]
+        if mac == "MAC":
+            continue
+        connected = connected.lower() == 'yes'
+        byMac[mac] = dict(mac=mac, connected=connected, auth=auth == 'Yes', ssid=ssid, iface=iface)
         
+    resp = yield fetch('http://10.2.0.2/DHCPTable.asp', headers=headers)
+    for row in re.findall(r'new AAA\((.*)\)', resp.body):
+        clientHostname, ipaddr, mac, expires, iface = [s.strip("'") for s in row.rsplit(',', 4)]
+        if clientHostname == 'wlanadv.none':
+            continue
+        byMac.setdefault(mac, {}).update(dict(
+            clientHostname=clientHostname, connection=iface, ipaddr=ipaddr, dhcpExpires=expires))
+    
+    returnValue(sorted(byMac.values()))
+    
             
 def jsValue(js, variableName):
     # using literal_eval instead of json parser to handle the trailing commas