Mercurial > code > home > repos > homeauto
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