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