Mercurial > code > home > repos > homeauto
changeset 115:c860b8c10de9
move watchpins from /room, add a graph
Ignore-this: 32e58e567fb1fc0567fd2877319aaeff
author | drewp@bigasterisk.com |
---|---|
date | Mon, 16 Sep 2013 08:20:18 -0700 |
parents | 4cd065b97fa1 |
children | fd3eda282b66 |
files | service/theaterArduino/watchpins.html service/theaterArduino/watchpins.py |
diffstat | 2 files changed, 344 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/theaterArduino/watchpins.html Mon Sep 16 08:20:18 2013 -0700 @@ -0,0 +1,98 @@ +<html> + <head> + </head> + <body> + theater motion sensor history: + <div id="chart_div" style="width: 900px; height: 250px;"></div> +max age: <input type="range" name="maxAge" min="10" max="300" value="180"> + <div id="status"></div> + <script type="text/javascript" src="https://www.google.com/jsapi"></script> + <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script> + <script type="text/javascript"> + google.load("visualization", "1", {packages:["corechart"]}); + +var latestData = null; +var chart = null; + + function loop() { + getNewPoints(); + setTimeout(function () { webkitRequestAnimationFrame(loop); }, 1200); + } + google.setOnLoadCallback(function () { + chart = new google.visualization.LineChart(document.getElementById('chart_div')) + loop(); + }); + + function getValue(subj, pred, trig) { + var groups = trig.match('<'+subj+'> <'+pred+'> "(.*?)"'); + if (groups == null) { + return null; + } + return groups[1]; + } + + function getNewPoints() { + $("#status").text("get graph..."); + $.ajax({ + url: "graph", + success: function(data) { + $("#status").text(""); + var pointsJson = getValue( + "http://projects.bigasterisk.com/device/theaterDoorOutsideMotion", + "http://projects.bigasterisk.com/room/history", + data); + var realPoints = []; + if (pointsJson != null) { + realPoints = JSON.parse(pointsJson); + } + var steppedPoints = new google.visualization.DataTable(); + steppedPoints.addColumn('number', 't'); + steppedPoints.addColumn('number', 'value'); + + var prev = null; + realPoints.forEach(function(r) { + if (prev) { + steppedPoints.addRows([[r[0], prev[1]]]); + } + steppedPoints.addRows([[r[0], r[1]]]); + prev = r; + }); + latestData = steppedPoints; + redraw(); + } + }); + } + $("input[name=maxAge]").change(redraw); + + function redraw() { + // https://developers.google.com/chart/interactive/docs/gallery/linechart#Configuration_Options + var options = { + title: 'theaterDoorOutsideMotion', + legend: { + position: "none", + }, + lineWidth: .5, + hAxis: { + title: "seconds ago", + viewWindow: { + min: -$("input[name=maxAge]").val(), + max: 0 + }, + gridlines: { count: -1 } + }, + vAxis: { + baseline: -999, + title: "motion sensed", + viewWindow: { + min: -.5, + max: 1.5, + } + } + }; + + chart.draw(latestData, options); + } + + </script> + </body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/theaterArduino/watchpins.py Mon Sep 16 08:20:18 2013 -0700 @@ -0,0 +1,246 @@ +""" +listener to the POST messages sent by theaterArduino.py when a pin changes. +records interesting events to mongodb, sends further messages. + +Will also serve activity stream. +""" +import sys, os, datetime, cyclone.web, simplejson, time +from twisted.internet import reactor +from twisted.internet.error import ConnectError +from twisted.internet.defer import inlineCallbacks, returnValue +from twisted.web.client import getPage +from dateutil.tz import tzutc +from pymongo import Connection +from rdflib import Namespace, Literal, Graph +from rdflib.parser import StringInputSource +sys.path.append("/my/site/magma") +from activitystream import ActivityStream +from stategraph import StateGraph + +sys.path.append("/my/proj/homeauto/lib") +from cycloneerr import PrettyErrorHandler +from logsetup import log + +DEV = Namespace("http://projects.bigasterisk.com/device/") +ROOM = Namespace("http://projects.bigasterisk.com/room/") +zeroTime = datetime.datetime.fromtimestamp(0, tzutc()) + +class PinChange(PrettyErrorHandler, cyclone.web.RequestHandler): + def post(self): + # there should be per-pin debounce settings so we don't log + # all the noise of a transition change + + msg = simplejson.loads(self.request.body) + msg['t'] = datetime.datetime.now(tzutc()) + msg['name'] = {9: 'downstairsDoorOpen', + 10: 'downstairsDoorMotion', + }[msg['pin']] + log.info("pinchange post %r", msg) + self.settings.mongo.insert(msg) + + history = self.settings.history + if msg['pin'] == 10: + history['motionHistory'] = (history.get('motionHistory', []) + [(msg['t'], msg['level'])])[-50:] + if msg['level'] == 1: + if history.get('prevMotion', 0) == 0: + history['motionStart'] = msg['t'] + + history['prevMotion'] = msg['level'] + + +class InputChange(PrettyErrorHandler, cyclone.web.RequestHandler): + """ + several other inputs post to here to get their events recorded, + too. This file shouldn't be in theaterArduino. See bedroomArduino, + frontDoorArduino, garageArduino. + """ + def post(self): + msg = simplejson.loads(self.request.body) + msg['t'] = datetime.datetime.now(tzutc()) + log.info(msg) + self.settings.mongo.insert(msg) + + # trigger to entrancemusic? rdf graph change PSHB? + +class GraphHandler(PrettyErrorHandler, cyclone.web.RequestHandler): + """ + fetch the pins from drv right now (so we don't have stale data), + and return an rdf graph describing what we know about the world + """ + @inlineCallbacks + def get(self): + g = StateGraph(ctx=DEV['houseSensors']) + + frontDoorDefer = getPage("http://slash:9080/door", timeout=2) # head start? + + doorOpen = int((yield getPage("http://bang:9056/pin/d9", timeout=1))) + g.add((DEV['theaterDoorOpen'], ROOM['state'], + ROOM['open'] if doorOpen else ROOM['closed'])) + + for s in self.motionStatements( + currentMotion=int((yield getPage("http://bang:9056/pin/d10", + timeout=1)))): + g.add(s) + + try: + for s in (yield self.getBedroomStatements()): + g.add(s) + except ConnectError, e: + g.add((ROOM['bedroomStatementFetch'], ROOM['error'], + Literal("getBedroomStatements: %s" % e))) + + try: + frontDoor = yield frontDoorDefer + g.add((DEV['frontDoorOpen'], ROOM['state'], + ROOM[frontDoor] if frontDoor in ['open', 'closed'] else + ROOM['error'])) + except Exception, e: + g.add((DEV['frontDoorOpen'], ROOM['error'], Literal(str(e)))) + + self.set_header('Content-type', 'application/x-trig') + self.write(g.asTrig()) + + def motionStatements(self, currentMotion): + uri = DEV['theaterDoorOutsideMotion'] + + yield (uri, ROOM['state'], ROOM['motion'] if currentMotion else ROOM['noMotion']) + + now = datetime.datetime.now(tzutc()) + if currentMotion: + try: + dt = now - self.settings.history['motionStart'] + yield (uri, ROOM['motionDurationSec'], Literal(dt.total_seconds())) + if dt > datetime.timedelta(seconds=4): + yield (uri, ROOM['state'], ROOM['sustainedMotion']) + except KeyError: + pass + + # this is history without the db, which means the window is + # limited and it could reset any time + if 'motionHistory' in self.settings.history: + yield ((uri, ROOM['history'], + Literal(simplejson.dumps( + [(round((t - now).total_seconds(), ndigits=2), v) + for t,v in self.settings.history['motionHistory']])))) + + @inlineCallbacks + def getBedroomStatements(self): + trig = yield getPage("http://bang:9088/graph", timeout=1) + stmts = set() + for line in trig.splitlines(): + if "http://projects.bigasterisk.com/device/bedroomMotion" in line: + g = Graph() + g.parse(StringInputSource(line+"\n"), format="nt") + for s in g: + stmts.add(s) + returnValue(stmts) + +class Activity(PrettyErrorHandler, cyclone.web.RequestHandler): + def get(self): + a = ActivityStream() + self.settings.mongo.ensure_index('t') + remaining = {'downstairsDoorMotion':10, 'downstairsDoorOpen':10, + 'frontDoorMotion':10, 'frontDoor':50, 'bedroomMotion': 10} + recent = {} + toAdd = [] + for row in list(self.settings.mongo.find(sort=[('t', -1)], + limit=5000)): + try: + r = remaining[row['name']] + if r < 1: + continue + remaining[row['name']] = r - 1 + except KeyError: + pass + + # lots todo + if row['name'] == 'downstairsDoorMotion': + if row['level'] == 0: + continue + kw = dict( + actorUri="http://...", + actorName="downstairs door", + verbUri="...", + verbEnglish="sees", + objectUri="...", + objectName="backyard motion", + objectIcon="/magma/static/backyardMotion.png") + elif row['name'] == 'downstairsDoorOpen': + kw = dict(actorUri="http://bigasterisk.com/foaf/someone", + actorName="someone", + verbUri="op", + verbEnglish="opens" if row['level'] else "closes", + objectUri="...", + objectName="downstairs door", + objectIcon="/magma/static/downstairsDoor.png") + elif row['name'] == 'frontDoor': + kw = dict(actorUri="http://bigasterisk.com/foaf/someone", + actorName="someone", + verbUri="op", + verbEnglish="opens" if row['state']=='open' else "closes", + objectUri="...", + objectName="front door", + objectIcon="/magma/static/frontDoor.png") + elif row['name'] == 'frontDoorMotion': + if row['state'] == False: + continue + if 'frontDoorMotion' in recent: + pass#if row['t' + kw = dict( + actorUri="http://...", + actorName="front door", + verbUri="...", + verbEnglish="sees", + objectUri="...", + objectName="front yard motion", + objectIcon="/magma/static/frontYardMotion.png") + recent['frontDoorMotion'] = kw + elif row['name'] == 'bedroomMotion': + if not row['state']: + continue + kw = dict( + actorUri="http://...", + actorName="bedroom", + verbUri="...", + verbEnglish="sees", + objectUri="...", + objectName="bedroom motion", + objectIcon="/magma/static/bedroomMotion.png") + recent['bedroomMotion'] = kw + else: + raise NotImplementedError(row) + + kw.update({'published' : row['t'], + 'entryUriComponents' : ('sensor', row['board'])}) + toAdd.append(kw) + + toAdd.reverse() + for kw in toAdd: + a.addEntry(**kw) + + self.set_header("Content-type", "application/atom+xml") + self.write(a.makeAtom()) + +class Application(cyclone.web.Application): + def __init__(self): + handlers = [ + (r'/()', cyclone.web.StaticFileHandler, + {"path" : ".", "default_filename" : "watchpins.html"}), + (r'/pinChange', PinChange), + (r'/inputChange', InputChange), + (r'/activity', Activity), + (r'/graph', GraphHandler), + ] + settings = { + 'mongo' : Connection('bang', 27017, + tz_aware=True)['house']['sensor'], + 'history' : { + }, + } + cyclone.web.Application.__init__(self, handlers, **settings) + +if __name__ == '__main__': + #from twisted.python import log as twlog + #twlog.startLogging(sys.stdout) + reactor.listenTCP(9069, Application()) + reactor.run()