diff service/tomatoWifi/tomatoWifi.py @ 0:6fd208b97616

start Ignore-this: e06ac598970a0d4750f588ab89f56996
author Drew Perttula <drewp@bigasterisk.com>
date Mon, 01 Aug 2011 03:30:30 -0700
parents
children 26a6cf58743d
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/tomatoWifi/tomatoWifi.py	Mon Aug 01 03:30:30 2011 -0700
@@ -0,0 +1,215 @@
+#!/usr/bin/python
+"""
+scrape the tomato router status pages to see who's connected to the
+wifi access points. Includes leases that aren't currently connected.
+
+Returns:
+ json listing (for magma page)
+ rdf graph (for reasoning)
+ activity stream, when we start saving history
+
+Todo: this should be the one polling and writing to mongo, not entrancemusic
+"""
+from __future__ import division
+import sys, cyclone.web, simplejson, traceback, time, pystache, datetime
+from dateutil import tz
+from twisted.python import log
+from twisted.internet import reactor, task
+
+from pymongo import Connection, DESCENDING
+from rdflib import Namespace, Literal, URIRef
+sys.path.append("/my/site/magma")
+from stategraph import StateGraph
+from wifi import Wifi
+
+sys.path.append("/my/proj/homeauto/lib")
+from cycloneerr import PrettyErrorHandler
+
+DEV = Namespace("http://projects.bigasterisk.com/device/")
+ROOM = Namespace("http://projects.bigasterisk.com/room/")
+
+class Index(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+
+        age = time.time() - self.settings.poller.lastPollTime
+        if age > 10:
+            raise ValueError("poll data is stale. age=%s" % age)
+        
+        self.write("this is wifiusage. needs index page that embeds the table")
+
+class Table(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        def rowDict(addr):
+            addr['cls'] = "signal" if addr.get('signal') else "nosignal"
+            if 'lease' in addr:
+                addr['lease'] = addr['lease'].replace("0 days, ", "")
+            return addr
+
+        self.set_header("Content-Type", "application/xhtml+xml")
+        self.write(pystache.render(
+            open("table.mustache").read(),
+            dict(
+                rows=sorted(map(rowDict, self.settings.poller.lastAddrs),
+                            key=lambda a: (a.get('router'),
+                                           a.get('name'),
+                                           a.get('mac'))))))
+
+        
+class Json(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        self.set_header("Content-Type", "application/json")
+        age = time.time() - self.settings.poller.lastPollTime
+        if age > 10:
+            raise ValueError("poll data is stale. age=%s" % age)
+        self.write(simplejson.dumps({"wifi" : self.settings.poller.lastAddrs,
+                                     "dataAge" : age}))
+
+class GraphHandler(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        g = StateGraph(ctx=DEV['wifi'])
+
+        # someday i may also record specific AP and their strength,
+        # for positioning. But many users just want to know that the
+        # device is connected to some bigasterisk AP.
+        aps = URIRef("http://bigasterisk.com/wifiAccessPoints")
+        age = time.time() - self.settings.poller.lastPollTime
+        if age > 10:
+            raise ValueError("poll data is stale. age=%s" % age)
+
+        for dev in self.settings.poller.lastAddrs:
+            if not dev.get('signal'):
+                continue
+            uri = URIRef("http://bigasterisk.com/wifiDevice/%s" % dev['mac'])
+            g.add((uri, ROOM['macAddress'], Literal(dev['mac'])))
+            g.add((uri, ROOM['connected'], aps))
+            if 'rawName' in dev:
+                g.add((uri, ROOM['wifiNetworkName'], Literal(dev['rawName'])))
+            g.add((uri, ROOM['deviceName'], Literal(dev['name'])))
+            g.add((uri, ROOM['signalStrength'], Literal(dev['signal'])))
+
+        self.set_header('Content-type', 'application/x-trig')
+        self.write(g.asTrig())
+       
+class Application(cyclone.web.Application):
+    def __init__(self, wifi, poller):
+        handlers = [
+            (r"/", Index),
+            (r'/json', Json),
+            (r'/graph', GraphHandler),
+            (r'/table', Table),
+            #(r'/activity', Activity),
+        ]
+        settings = {
+            'wifi' : wifi,
+            'poller' : poller, 
+            'mongo' : Connection('bang', 27017,
+                                 tz_aware=True)['house']['sensor']
+            }
+        cyclone.web.Application.__init__(self, handlers, **settings)
+
+class Poller(object):
+    def __init__(self, wifi, mongo):
+        self.wifi = wifi
+        self.mongo = mongo
+        self.lastAddrs = []
+        self.lastWithSignal = []
+        self.lastPollTime = 0
+
+    def assertCurrent(self):
+        dt = time.time() - self.lastPollTime
+        assert dt < 10, "last poll was %s sec ago" % dt
+        
+    def poll(self):
+        try:
+            newAddrs = self.wifi.getPresentMacAddrs()
+
+            newWithSignal = [a for a in newAddrs if a.get('signal')]
+
+            actions = self.computeActions(newWithSignal)
+            for action in actions:
+                action['created'] = datetime.datetime.now(tz.gettz('UTC'))
+                mongo.save(action)
+                try:
+                    self.doEntranceMusic(action)
+                except Exception, e:
+                    print "entrancemusic error", e
+                    
+            self.lastWithSignal = newWithSignal
+            self.lastAddrs = newAddrs
+            self.lastPollTime = time.time()
+        except Exception, e:
+            print "poll error", e
+            traceback.print_exc()
+
+    def computeActions(self, newWithSignal):
+        def removeVolatile(a):
+            ret = dict((k,v) for k,v in a.items() if k in ['name', 'mac'])
+            ret['signal'] = bool(a.get('signal'))
+            return ret
+
+        def find(a, others):
+            a = removeVolatile(a)
+            return any(a == removeVolatile(o) for o in others)
+
+        actions = []
+
+        def makeAction(addr, act):
+            return dict(sensor="wifi",
+                        address=addr.get('mac'),
+                        name=addr.get('name'),
+                        networkName=addr.get('rawName'),
+                        action=act)
+
+        for addr in newWithSignal:
+            if not find(addr, self.lastWithSignal):
+                # the point of all the removeVolatile stuff is so
+                # I have the complete addr object here, although
+                # it is currently mostly thrown out by makeAction
+                actions.append(makeAction(addr, 'arrive'))
+
+        for addr in self.lastWithSignal:
+            if not find(addr, newWithSignal):
+                actions.append(makeAction(addr, 'leave'))
+
+        return actions
+
+
+    def doEntranceMusic(self, action):
+        # these need to move out to their own service
+        dt = self.deltaSinceLastArrive(action['name'])
+        if dt > datetime.timedelta(hours=1):
+            import restkit, jsonlib
+            hub = restkit.Resource(
+                # PSHB not working yet; "http://bang:9030/"
+                "http://slash:9049/"
+                )
+            action = action.copy()
+            del action['created']
+            hub.post("visitorNet", payload=jsonlib.dumps(action))
+
+    def deltaSinceLastArrive(self, name):
+        results = list(self.mongo.find({'name' : name}).sort('created',
+                                                         DESCENDING).limit(1))
+        if not results:
+            return datetime.timedelta.max
+        now = datetime.datetime.now(tz.gettz('UTC'))
+        last = results[0]['created'].replace(tzinfo=tz.gettz('UTC'))
+        return now - last
+        
+
+if __name__ == '__main__':
+    config = {
+        'servePort' : 9070,
+        'pollFrequency' : 1/5,
+        }
+    #log.startLogging(sys.stdout)
+
+
+    mongo = Connection('bang', 27017)['visitor']['visitor']
+
+    wifi = Wifi()
+    poller = Poller(wifi, mongo)
+    task.LoopingCall(poller.poll).start(1/config['pollFrequency'])
+
+    reactor.listenTCP(config['servePort'], Application(wifi, poller))
+    reactor.run()