changeset 815:7b0d282b292d

prettier viewer of recent BT activity. don't announce BT with no names. don't announce leaving BT Ignore-this: ad7b99b1259dc1501179a70faca5454c darcs-hash:20110816072302-312f9-6af13fa96f1d70c2065bf5fec84da7574fee88a5.gz
author drewp <drewp@bigasterisk.com>
date Tue, 16 Aug 2011 00:23:02 -0700
parents 84f336f69004
children 926adf586b08
files service/bluetooth/bluetoothService.py service/bluetooth/index.xhtml
diffstat 2 files changed, 225 insertions(+), 18 deletions(-) [+]
line wrap: on
line diff
--- a/service/bluetooth/bluetoothService.py	Tue Aug 16 00:22:41 2011 -0700
+++ b/service/bluetooth/bluetoothService.py	Tue Aug 16 00:23:02 2011 -0700
@@ -15,7 +15,9 @@
 
 """
 from __future__ import absolute_import
-import logging, time, datetime, restkit, jsonlib, cyclone.web, sys
+import logging, time, datetime, restkit, jsonlib, sys, socket
+import cyclone.web, pystache
+from dateutil.tz import tzutc, tzlocal
 from bluetooth import discover_devices, lookup_name
 from twisted.internet import reactor, task
 from twisted.internet.threads import deferToThread
@@ -28,18 +30,35 @@
 from cycloneerr import PrettyErrorHandler
 from logsetup import log
 
-mongo = Connection('bang', 27017)['visitor']['visitor']
+mongo = Connection('bang', 27017, tz_aware=True)['visitor']['visitor']
 
 ROOM = Namespace("http://projects.bigasterisk.com/room/")
 
+# the mongodb serves as a much bigger cache, but I am expecting that
+# 1) i won't fill memory with too many names; 2) this process will see
+# each new device before it leaves, so I'll have the leaving name in
+# my cache
+nameCache = {} # addr : name
+
+def lookupPastName(addr):
+    row = mongo.find_one({"address" : addr,
+                          'name' : {'$exists' : True}},
+                         sort=[("created",-1)])
+    if row is None:
+        return None
+    return row['name']
+
 def getNearbyDevices():
     addrs = discover_devices()
 
-    # this can be done during discover_devices, but my plan was to
-    # cache it more in here
-    names = dict((a, lookup_name(a)) for a in addrs)
-    log.debug("discover found %r %r", addrs, names)
-    return addrs, names
+    for a in addrs:
+        if a not in nameCache:
+            n = lookup_name(a) or lookupPastName(a)
+            if n is not None:
+                nameCache[a] = n
+                
+    log.debug("discover found %r", addrs)
+    return addrs
 
 hub = restkit.Resource(
     # PSHB not working yet; "http://bang:9030/"
@@ -52,7 +71,7 @@
     except UnicodeDecodeError:
         pass
     else:
-        if msg['name'] != 'THINKPAD_T43':
+        if msg.get('name', '') and msg['name'] not in ['THINKPAD_T43']:
             hub.post("visitorNet", payload=js) # sans datetime
     msg['created'] = datetime.datetime.now(tz.gettz('UTC'))
     mongo.insert(msg, safe=True)
@@ -74,29 +93,29 @@
         devs.addErrback(log.error)
         return devs
 
-    def compare(self, (addrs, names)):
+    def compare(self, addrs):
         self.lastPollTime = time.time()
 
         newGraph = Graph()
         addrs = set(addrs)
         for addr in addrs.difference(self.lastAddrs):
-            self.recordAction('arrive', addr, names)
+            self.recordAction('arrive', addr)
         for addr in self.lastAddrs.difference(addrs):
-            self.recordAction('leave', addr, names)
+            self.recordAction('leave', addr)
         for addr in addrs:
             uri = deviceUri(addr)
             newGraph.add((ROOM['bluetooth'], ROOM['senses'], uri))
-            if addr in names:
-                newGraph.add((uri, RDFS.label, Literal(names[addr])))
+            if addr in nameCache:
+                newGraph.add((uri, RDFS.label, Literal(nameCache[addr])))
         self.lastAddrs = addrs
         self.currentGraph = newGraph
 
-    def recordAction(self, action, addr, names):
+    def recordAction(self, action, addr):
         doc = {"sensor" : "bluetooth",
                "address" : addr,
                "action" : action}
-        if addr in names:
-            doc["name"] = names[addr]
+        if addr in nameCache:
+            doc["name"] = nameCache[addr]
         log.info("action: %s", doc)
         mongoInsert(doc)
 
@@ -105,8 +124,37 @@
         age = time.time() - self.settings.poller.lastPollTime
         if age > self.settings.config['period'] + 30:
             raise ValueError("poll data is stale. age=%s" % age)
-        
-        self.write("bluetooth watcher. ")
+
+        self.set_header("Content-Type", "application/xhtml+xml")
+        self.write(pystache.render(
+            open("index.xhtml").read(),
+            dict(host=socket.gethostname(),
+                 )))
+
+class Recent(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        name = {}  # addr : name
+        events = []
+        hours = float(self.get_argument("hours", default="3"))
+        t1 = datetime.datetime.now(tzutc()) - datetime.timedelta(seconds=60*60*hours)
+        for row in mongo.find({"sensor":"bluetooth",
+                               "created":{"$gt":t1}}, sort=[("created", 1)]):
+            if 'name' in row:
+                name[row['address']] = row['name']
+            row['t'] = int(row['created'].astimezone(tzlocal()).strftime("%s"))
+            del row['created']
+            del row['_id']
+            events.append(row)
+
+        for r in events:
+            r['name'] = name.get(r['address'], r['address'])
+        self.set_header("Content-Type", "application/json")
+        self.write(jsonlib.dumps({"events" : events}))
+           
+
+class Static(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self, fn):
+        self.write(open(fn).read())
 
 if __name__ == '__main__':
     config = {
@@ -116,6 +164,8 @@
     poller = Poller()
     reactor.listenTCP(9077, cyclone.web.Application([
         (r'/', Index),
+        (r'/recent', Recent),
+        (r'/(underscore-min.js|pretty.js)', Static),
         # graph, json, table, ...
         ], poller=poller, config=config))
     task.LoopingCall(poller.poll).start(config['period'])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/bluetooth/index.xhtml	Tue Aug 16 00:23:02 2011 -0700
@@ -0,0 +1,157 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <title></title>
+    <style type="text/css" media="all">
+      /* <![CDATA[ */
+      body {
+	  font-family: sans-serif;
+	  font-size: 12px;
+      }
+      .stripChart {
+	  background: none repeat scroll 0 0 #EAEAEA;
+	  border-bottom: 1px solid #b7b7b7;
+	  display: inline-block;
+	  height: 22px;
+	  margin: 0;
+	  padding: 0;
+	  white-space: nowrap;
+      }
+      .chartArea {
+	  width: 300px;
+	  height: 20px;
+	  border: 1px solid gray;
+	  display: inline-block;
+	  position: relative;
+	  background: white;
+      }
+      .name {
+	  display: inline-block;
+	  width: 16em;
+      } 
+
+      .chartArea > span {
+	  position: absolute;
+	  height: 20px;
+	  background: #d3e1ed;
+	  background: -moz-linear-gradient(left, #d3e1ed 0%, #86aecc 100%);
+	  background: -webkit-gradient(linear, left top, right top, color-stop(0%,#d3e1ed), color-stop(100%,#86aecc));
+	  background: -webkit-linear-gradient(left, #d3e1ed 0%,#86aecc 100%);
+	  background: -o-linear-gradient(left, #d3e1ed 0%,#86aecc 100%);
+	  background: linear-gradient(left, #d3e1ed 0%,#86aecc 100%);
+      }
+
+      .stripChart > .timeLeft, .stripChart > .timeRight { 
+	  font-size: 10px;
+	  vertical-align: super;
+	  padding: 0 4px;
+      }
+     
+      /* ]]> */
+    </style>
+
+  </head>
+  <body>
+    <h1>bluetooth watcher on {{host}}</h1>
+
+    <p>Recent activity</p>
+
+    <div id="activity"/>
+    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"/>
+      <script type="text/javascript" src="underscore-min.js"/>
+      <script type="text/javascript" src="pretty.js"/>
+
+    <script type="text/javascript">
+    // <![CDATA[
+    
+    function StripChart(parent) {
+        var strips = [[null, null]]; // [[starttime, endtime], ...]
+        this.addEvent = function (ev) {
+            // must pass events in order
+            if (ev.action == "arrive") {
+                if (_.isNull(_.last(strips)[1])) {
+		    if (_.isNull(_.last(strips)[0])) {
+			_.last(strips)[0] = ev.t;
+		    } else {
+			// two arrives in a row
+		    }
+                } else {
+                    strips.push([ev.t, null]);
+                }
+            }
+            if (ev.action == "leave") {
+                if (_.isNull(_.last(strips)[1])) {
+                    _.last(strips)[1] = ev.t;
+                } else {
+                    // two leaves in a row
+                }
+            }
+        };
+        function stripIsComplete(se) {
+            return !_.isNull(se[0]) && !_.isNull(se[1]); 
+        }
+	function displayTime(secs) {
+	    var iso = new Date(secs*1000).toJSON().replace(/\.\d+/,"");
+	    return prettyDate(iso);
+	}
+        this.draw = function () {
+	    var now = .001*new Date();
+            if (_.isNull(_.last(strips)[1])) {
+                _.last(strips)[1] = now;
+            }
+            var complete = _.select(strips, stripIsComplete);
+            if (_.isEmpty(complete)) {
+                return;
+            }
+            var t1 = _.first(complete)[0], t2 = now;
+            function xForTime(t) {
+                return 300 * (t - t1) / (t2 - t1)
+            }
+	    parent.append($("<span>").addClass("timeLeft").text(displayTime(t1)));
+	    var out = $("<span>").addClass("chartArea")
+	    parent.append(out);
+	    var lastX = 0;
+            $.each(complete, function (i, se) {
+                var x1 = xForTime(se[0]), x2 = xForTime(se[1]);
+		if (x1 < lastX) {
+		    // culls ok, but may leave gaps. I'd rather
+		    // something that joins the slivers intead of
+		    // skipping them
+		    return;
+		}
+		var w = Math.max(2, x2 - x1)
+                out.append($("<span>").css({left: x1, width: w}));
+		lastX = x1 + w;
+            });
+	    parent.append($("<span>").addClass("timeRight").text("now"));
+        };
+    }
+
+    $(function() { 
+        var addressRow = {}; // address : row
+        var chart = {} // address : StripChart
+
+        $.getJSON("recent", {hours:24*7}, function (data) {
+            
+            $.each(data.events, function (i, ev) {
+                if (!addressRow[ev.address]) {
+                    var row = $("<li>").append($("<span>").addClass("name").text(ev.name)).append($("<span>").addClass("stripChart"));
+                    $("#activity").append(row);
+                    addressRow[ev.address] = row;
+                    chart[ev.address] = new StripChart(row.find(".stripChart"));
+                }
+                chart[ev.address].addEvent(ev);
+            });
+            $.each(chart, function (k, c) {
+                c.draw();
+            });
+        });
+    });
+    // ]]>
+</script>
+
+
+  </body>
+</html>
\ No newline at end of file