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()