changeset 0:6fd208b97616

start Ignore-this: e06ac598970a0d4750f588ab89f56996
author Drew Perttula <drewp@bigasterisk.com>
date Mon, 01 Aug 2011 03:30:30 -0700
parents
children 26a6cf58743d
files service/deskArduino/desk/desk.pde service/deskArduino/deskArduino.py service/deskArduino/index.html service/environment/environment.py service/frontDoorArduino/frontDoorArduino.py service/frontDoorArduino/index.html service/frontDoorMessage/automsg.py service/frontDoorMessage/frontDoorMessage.py service/frontDoorMessage/index.html service/garageArduino/garage/garage.pde service/garageArduino/garageArduino.py service/garageArduino/index.html service/tomatoWifi/table.mustache service/tomatoWifi/tomatoWifi.py service/tomatoWifi/wifi.py
diffstat 15 files changed, 1407 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/deskArduino/desk/desk.pde	Mon Aug 01 03:30:30 2011 -0700
@@ -0,0 +1,97 @@
+int datapin  = 10; // DI
+int latchpin = 11; // LI
+int enablepin = 12; // EI
+int clockpin = 13; // CI
+
+unsigned long SB_CommandPacket;
+int SB_CommandMode;
+int SB_BlueCommand;
+int SB_RedCommand;
+int SB_GreenCommand;
+
+#define SHIFT(val) shiftOut(datapin, clockpin, MSBFIRST, val)
+
+void SB_SendPacket() {
+  /* high bits are 00 for color, 01 for current */
+   SB_CommandPacket = SB_CommandMode & B11;
+   SB_CommandPacket = (SB_CommandPacket << 10)  | (SB_BlueCommand & 1023);
+   SB_CommandPacket = (SB_CommandPacket << 10)  | (SB_RedCommand & 1023);
+   SB_CommandPacket = (SB_CommandPacket << 10)  | (SB_GreenCommand & 1023);
+
+   SHIFT(SB_CommandPacket >> 24);
+   SHIFT(SB_CommandPacket >> 16);
+   SHIFT(SB_CommandPacket >> 8);
+   SHIFT(SB_CommandPacket);
+
+}
+void latch() {
+   delayMicroseconds(100);
+   digitalWrite(latchpin,HIGH); // latch data into registers
+   delayMicroseconds(100);
+   digitalWrite(latchpin,LOW); 
+}
+
+void setCurrent(byte r, byte g, byte b) { 
+ /* 127 = max */ 
+   SB_CommandMode = B01; // Write to current control registers
+   SB_RedCommand = r; 
+   SB_GreenCommand = g;
+   SB_BlueCommand = b;
+   SB_SendPacket();
+   latch();
+}
+
+void setup() {
+   pinMode(datapin, OUTPUT);
+   pinMode(latchpin, OUTPUT);
+   pinMode(enablepin, OUTPUT);
+   pinMode(clockpin, OUTPUT);
+
+   digitalWrite(latchpin, LOW);
+   digitalWrite(enablepin, LOW);
+
+   for (int i=0; i < 2; i++) {
+     setCurrent(127, 127, 127);
+   }
+
+   SHIFT(0x3f); SHIFT(0xc0); SHIFT(0x00); SHIFT(0x00);
+   SHIFT(0x00); SHIFT(0x0f); SHIFT(0xf0); SHIFT(0x00);
+   latch();
+
+   Serial.begin(115200);
+   Serial.flush();
+}
+
+void loop() {
+  byte head, cmd;
+  if (Serial.available() >= 2) {
+    head = Serial.read();
+    if (head != 0x60) {
+      Serial.flush();
+      return;
+    }
+    cmd = Serial.read();
+    if (cmd == 0x00) {
+      Serial.print("{\"ok\":\"ping\"}\n");
+    } else if (cmd == 0x01) {
+      /*
+	one byte for the string length, then a buffer to be shifted
+	out to all the shiftbrites
+      */
+
+      while (Serial.available() == 0) NULL;
+      byte count = Serial.read();
+      /*
+      for (int i=0; i < count / 4; i++) {
+	setCurrent(127, 127, 127);
+      }
+      */
+      for (int i=0; i<count; i++) {
+	while (Serial.available() == 0) NULL;
+	SHIFT(Serial.read());
+      }
+      latch();
+      Serial.print("{\"ok\":1}\n");
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/deskArduino/deskArduino.py	Mon Aug 01 03:30:30 2011 -0700
@@ -0,0 +1,99 @@
+#!bin/python
+"""
+talks to shiftbrite driver on dash, plus future arduino stuff
+"""
+
+from __future__ import division
+
+import cyclone.web, sys, bitstring
+from twisted.python import log
+from twisted.internet import reactor
+from rdflib import Namespace
+sys.path.append("/my/proj/house/frontdoor")
+from loggingserial import LoggingSerial        
+sys.path.append("/my/site/magma")
+from stategraph import StateGraph      
+sys.path.append("/my/proj/homeauto/lib")
+from cycloneerr import PrettyErrorHandler
+
+ROOM = Namespace("http://projects.bigasterisk.com/room/")
+DEV = Namespace("http://projects.bigasterisk.com/device/")
+
+from webcolors import hex_to_rgb, rgb_to_hex
+
+def rgbFromHex(h):
+    """returns tuple of 0..1023"""
+    norm = hex_to_rgb(h)
+    return tuple([x * 4 for x in norm])
+
+def hexFromRgb(rgb):
+    return rgb_to_hex(tuple([x // 4 for x in rgb]))
+
+class ArduinoDesk(object):
+    def __init__(self, ports=['/dev/ttyUSB0', '/dev/ttyUSB1']):
+        self.ser = LoggingSerial(ports=ports, baudrate=115200, timeout=1)
+
+    def ping(self):
+        self.ser.write("\x60\x00")
+        msg = self.ser.readJson()
+        assert msg == {"ok":"ping"}, msg
+
+    def shiftbrite(self, colors):
+        """
+        shift out this sequence of (r,g,b) triples of 10-bit ints
+        """
+        out = "".join(bitstring.pack("0b00, uint:10, uint:10, uint:10",
+                                     b, r, g).bytes
+                      for r,g,b in colors)
+
+        self.ser.write("\x60\x01" + chr(len(out)) + out)
+        msg = self.ser.readJson()
+        assert msg == {"ok":1}, msg
+        
+class Index(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        self.settings.arduino.ping()
+        
+        self.set_header("Content-Type", "application/xhtml+xml")
+        self.write(open("index.html").read())
+
+class GraphPage(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        self.set_header("Content-Type", "application/x-trig")
+        g = StateGraph(ROOM['deskArduino'])
+        # g.add((s,p,o)) for colors and stuff      
+        self.write(g.asTrig())
+
+class Brite(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self, chan):
+        self.set_header("Content-Type", "text/plain")
+        self.write(hexFromRgb(self.settings.colors[int(chan)]))
+        
+    def put(self, chan):
+        s = self.settings
+        s.colors[int(chan)] = rgbFromHex(self.request.body)
+        s.arduino.shiftbrite(s.colors)
+    post = put
+
+class Application(cyclone.web.Application):
+    def __init__(self, arduino):
+        handlers = [
+            (r"/", Index),
+            (r"/graph", GraphPage),
+            (r"/brite/(\d+)", Brite),
+        ]
+        colors = [(0,0,0)] * 2 # stored 10-bit
+        cyclone.web.Application.__init__(self, handlers,
+                                         arduino=arduino, colors=colors)
+
+if __name__ == '__main__':
+    config = { # to be read from a file
+        'servePort' : 9014,
+        }
+
+    #log.startLogging(sys.stdout)
+
+    arduino = ArduinoDesk()
+
+    reactor.listenTCP(config['servePort'], Application(arduino))
+    reactor.run()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/deskArduino/index.html	Mon Aug 01 03:30:30 2011 -0700
@@ -0,0 +1,54 @@
+<?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>desk arduino</title>
+    <style type="text/css" media="all">
+      /* <![CDATA[ */
+      body {
+	  background: #000000;
+	  color: #888;
+      }
+      h1 {
+	  font-size:100%;
+      }
+      div.credit {
+	  font-size: 80%;
+	  margin-top:41px;
+      }
+      /* ]]> */
+    </style>
+
+    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
+    <script type="text/javascript" src="http://bigasterisk.com/bathroomLight/static/farbtastic/farbtastic.js"></script>
+    <link rel="stylesheet" href="http://bigasterisk.com/bathroomLight/static/farbtastic/farbtastic.css" type="text/css" />
+
+  </head>
+  <body>
+    <h1>desk arduino</h1>
+
+    <div class="colorpicker" chan="0"></div>
+    <div class="colorpicker" chan="1"></div>
+    
+    <script type="text/javascript">
+      $(document).ready(function() {
+	  $(".colorpicker").each(function (elem) {
+	      var chan = $(this).attr("chan");
+	      
+	      var picker = $.farbtastic(this, function (newColor) {
+		  $.post('brite/'+chan, newColor);
+	      });
+	      
+	      $.get('brite/'+chan, picker.setColor);
+	  });
+      });
+    </script>
+    
+    <div class="credit">
+      Using the 
+      <a href="http://acko.net/dev/farbtastic">Farbtastic color picker</a>
+    </div>
+
+  </body>
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/environment/environment.py	Mon Aug 01 03:30:30 2011 -0700
@@ -0,0 +1,43 @@
+#!/usr/bin/python
+"""
+return some rdf about the environment, e.g. the current time,
+daytime/night, overall modes like 'maintenance mode', etc
+
+"""
+import sys, datetime, cyclone.web
+from twisted.internet import reactor
+from dateutil.tz import tzlocal
+from rdflib import Namespace, Literal
+sys.path.append("/my/site/magma")
+from stategraph import StateGraph
+sys.path.append("/my/proj/homeauto/lib")
+from cycloneerr import PrettyErrorHandler
+
+ROOM = Namespace("http://projects.bigasterisk.com/room/")
+DEV = Namespace("http://projects.bigasterisk.com/device/")
+
+class Index(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        self.write('this is envgraph: <a href="graph">rdf</a>')
+        
+class GraphHandler(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        g = StateGraph(ROOM.environment)
+        now = datetime.datetime.now(tzlocal())
+
+        g.add((DEV.environment, ROOM.localHour, Literal(now.hour)))
+        
+        self.set_header('Content-type', 'application/x-trig')
+        self.write(g.asTrig())
+        
+class Application(cyclone.web.Application):
+    def __init__(self):
+        handlers = [
+            (r"/", Index),
+            (r'/graph', GraphHandler),
+        ]
+        cyclone.web.Application.__init__(self, handlers)
+
+if __name__ == '__main__':
+    reactor.listenTCP(9075, Application())
+    reactor.run()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/frontDoorArduino/frontDoorArduino.py	Mon Aug 01 03:30:30 2011 -0700
@@ -0,0 +1,179 @@
+"""
+talks to the arduino outside the front door. Don't write straight to
+this LCD; use frontDoorMessage for that.
+
+lcd is this wide
+|-------------------|
+22:05 85F in, 71F out
+
+"""
+
+from __future__ import division
+
+import cyclone.web, json, traceback, os, sys
+from twisted.python import log
+from twisted.internet import reactor, task
+from twisted.web.client import getPage
+
+sys.path.append("/my/proj/house/frontdoor")
+from loggingserial import LoggingSerial        
+
+sys.path.append("/my/proj/homeauto/lib")
+from cycloneerr import PrettyErrorHandler
+
+class Board(object):
+    """
+    arduino board actions, plus the last values we wrote to it
+    """
+    def __init__(self, port):
+        self.ser = LoggingSerial(port=port)
+        self.ser.flush()
+
+        self.ser.write("\xff\x00\x00")
+        self.ser.write("\xff\x03\x00")
+        self.currentText = ""
+        self.currentBrightness = 0
+
+    def ping(self):
+        self.getDoor()
+
+    def getDoor(self):
+        self.ser.write("\xff\x01")
+        ret = self.ser.readJson()
+        return ret['door']
+
+    def getLcd(self):
+        return self.currentText
+        
+    def setLcd(self, txt):
+        """
+        up to 8*21 chars
+        """
+        self.currentText = txt
+        self.ser.write("\xff\x00" + txt + "\x00")
+
+    def getLcdBrightness(self):
+        return self.currentBrightness
+
+    def setLcdBrightness(self, b):
+        """b in 0 to 255"""
+        self.currentBrightness = b
+        self.ser.write("\xff\x03" + chr(b))
+
+    def getTemperature(self):
+        """returns parsed json from the board"""
+        self.ser.write("\xff\x02")
+        # this can take 1.25 seconds per retry
+        f = self.ser.readJson()
+
+        if f['temp'] > 184 or f['temp'] < -100:
+            # this fails a lot, maybe 50% of the time. retry if 
+            # you want
+            raise ValueError("out of range temp value (%s)" % f)
+        return f
+    
+class index(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        self.settings.board.ping()
+        
+        self.set_header("Content-Type", "application/xhtml+xml")
+        self.write(open("index.html").read())
+
+class Lcd(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        self.set_header("Content-Type", "text/plain")
+        self.write(self.settings.board.getLcd())
+        
+    def put(self):
+        self.settings.board.setLcd(self.request.body)
+        self.set_status(204)
+
+class Backlight(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        self.set_header("Content-Type", "application/json")
+        self.write(json.dumps({
+            "backlight" : self.settings.board.getLcdBrightness()}))
+        
+    def put(self):
+        """param brightness=0 to brightness=255"""
+        self.settings.board.setLcdBrightness(
+            int(self.get_argument('brightness')))
+        self.write("ok")
+    post = put
+
+class Door(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        self.set_header("Content-Type", "text/plain")
+        self.write(self.settings.board.getDoor())
+
+class Temperature(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        f = self.settings.board.getTemperature()
+        self.set_header("Content-Type", "application/json")        
+        self.write(f)
+        
+class Application(cyclone.web.Application):
+    def __init__(self, board):
+        handlers = [
+            (r"/", index),
+            (r'/lcd', Lcd),
+            (r'/door', Door),
+            (r'/temperature', Temperature),
+            (r'/lcd/backlight', Backlight),
+        ]
+        settings = {"board" : board}
+        cyclone.web.Application.__init__(self, handlers, **settings)
+
+
+class Poller(object):
+    def __init__(self, board, postUrl, boardName):
+        self.board = board
+        self.postUrl = postUrl
+        self.boardName = boardName
+        self.last = None
+
+    def poll(self):
+        try:
+            new = self.board.getDoor()
+            if new != self.last:
+                msg = json.dumps(dict(board=self.boardName, 
+                                      name="frontDoor", state=new))
+                getPage(self.postUrl,
+                        method="POST",
+                        postdata=msg,
+                        headers={'Content-Type' : 'application/json'}
+                        ).addErrback(self.reportError, msg)
+
+            self.last = new
+        except (IOError, OSError):
+            os.abort()
+        except Exception, e:
+            print "poll error", e
+            traceback.print_exc()
+            
+    def reportError(self, msg, *args):
+        print "post error", msg, args
+
+if __name__ == '__main__':
+
+    port = '/dev/ttyUSB0'
+    if not os.path.exists(port):
+        port = '/dev/ttyUSB1'
+
+    config = { # to be read from a file
+        'arduinoPort': port,
+        'servePort' : 9080,
+        'pollFrequency' : 1,
+        'boardName' : 'frontDoor', # gets sent with updates
+        'doorChangePost' : 'http://bang.bigasterisk.com:9069/inputChange',
+        # todo: need options to preset inputs/outputs at startup
+        }
+
+    log.startLogging(sys.stdout)
+
+    board = Board(port=config['arduinoPort'])
+    
+    p = Poller(board, config['doorChangePost'], config['boardName'])
+    task.LoopingCall(p.poll).start(1/config['pollFrequency'])
+    reactor.listenTCP(config['servePort'], Application(board))
+    reactor.run()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/frontDoorArduino/index.html	Mon Aug 01 03:30:30 2011 -0700
@@ -0,0 +1,91 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <title>front door</title>
+    <link rel="Stylesheet" type="text/css" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.0/themes/ui-lightness/jquery-ui.css" media="all"/>
+    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.min.js"></script>
+      <style type="text/css" media="all">
+	/* <![CDATA[ */
+	#message, #lastLine {
+background: #C5F180; color: #0A08A2; font-weight: bold;
+font-family: monospace;
+}
+section {
+   background: none repeat scroll 0 0 #E1E1DF;
+    border: 1px solid #595555;
+    float: left;
+    margin: 20px;
+    padding: 20px;
+}
+	/* ]]> */
+</style>
+    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.11/jquery-ui.min.js"></script>
+  </head>
+  <body>
+    <section>
+      <h1>lcd</h1>
+      <div>
+	set message: 
+	<div>
+	  <textarea cols="21" rows="7" id="message"/>
+	</div>
+      </div>
+      <div>
+	backlight:
+	<div id="backlightSlider" style="width: 300px;"/>
+	  </div>
+    </section>
+    <section>
+      <h1>temperature</h1>
+      <div>Current: <span id="temperature"/> 
+      <button id="getTemperature">refresh</button>
+      </div>
+    </section>
+    <section>
+      <h1>door</h1>
+      <div>Current: <span id="door"/>
+      <button id="getDoor">refresh</button>
+      </div>
+    </section>
+    <script type="text/javascript">
+    // <![CDATA[
+    $(function () {
+	
+        $.get("lcd", function (data){ $("#message").val(data) });
+	$("#message").keyup(function() {
+	    $.ajax({
+		type: "PUT",
+		url: "lcd", 
+		data: $("#message").val()
+	    });
+	});
+	
+	$.getJSON("lcd/backlight", function (data) { 
+            $("#backlightSlider").slider({value: data.backlight});
+        });
+	$("#backlightSlider").slider({
+	    min: 0, max: 255, 
+	    slide: function (ev, ui) {
+		$.post("lcd/backlight", {brightness: ui.value});
+	    }});
+
+	function getTemperature() {
+	    $.get("temperature", function (data) { 
+		$("#temperature").text(data); 
+	    });
+	}
+	getTemperature();
+	$("#getTemperature").click(getTemperature);
+
+	function getDoor() {
+	    $.get("door", function (x) { $("#door").text(x) });
+	}
+	getDoor();
+	$("#getDoor").click(getDoor);
+
+    });
+	    // ]]>
+</script>
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/frontDoorMessage/automsg.py	Mon Aug 01 03:30:30 2011 -0700
@@ -0,0 +1,23 @@
+"""
+write the automatic last line to LCD /lastLine
+"""
+import sys, time
+sys.path.append("/my/site/magma")
+from datetime import datetime
+from graphitetemp import getAllTemps
+
+import restkit
+
+# needs poller with status report
+
+while True:
+    fd = restkit.Resource("http://bang:9081/")
+
+    allTemp = getAllTemps()
+    now = datetime.now()
+
+    line = "%02d:%02d %02dF in, %02dF out" % (now.hour, now.minute,
+                                              allTemp['livingRoom'],
+                                              allTemp.get('frontDoor', 0))
+    fd.put("lastLine", payload=line)
+    time.sleep(60)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/frontDoorMessage/frontDoorMessage.py	Mon Aug 01 03:30:30 2011 -0700
@@ -0,0 +1,80 @@
+"""
+holds the current message on the front door lcd
+"""
+import cyclone.web, sys
+import restkit
+from twisted.python import log
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks
+sys.path.append("/my/proj/homeauto/lib")
+from cycloneerr import PrettyErrorHandler
+
+class LcdParts(object):
+    def __init__(self, putUrl):
+        self.putUrl = putUrl
+        self.message = ""
+        self.lastLine = ""
+
+    def updateLcd(self):
+        whole = "%-147s%-21s" % (self.message, self.lastLine)
+        restkit.request(url=self.putUrl,
+                        method="PUT",
+                        body=whole,
+                        headers={"content-type":"text/plain"})
+        
+class Index(PrettyErrorHandler, cyclone.web.RequestHandler):
+    @inlineCallbacks
+    def get(self):
+
+        # refresh output, and make an error if we can't talk to them
+        yield self.settings.lcdParts.updateLcd()
+        
+        self.set_header("Content-Type", "application/xhtml+xml")
+        self.write(open("index.html").read())
+
+def getArg(s):
+    return s.request.body.encode("ascii")
+
+class Message(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        self.set_header("Content-Type", "text/plain")
+        self.write(self.settings.lcdParts.message)
+
+    def put(self):
+        self.settings.lcdParts.message = getArg(self)
+        self.settings.lcdParts.updateLcd()
+        self.set_status(204)
+
+class LastLine(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        self.set_header("Content-Type", "text/plain")
+        self.write(self.settings.lcdParts.lastLine)
+
+    def put(self):
+        self.settings.lcdParts.lastLine = getArg(self)
+        self.settings.lcdParts.updateLcd()
+        self.set_status(204)
+
+class Application(cyclone.web.Application):
+    def __init__(self, lcdParts):
+        handlers = [
+            (r"/", Index),
+            (r"/message", Message),
+            (r'/lastLine', LastLine),
+        ]
+        settings = {"lcdParts" : lcdParts}
+        cyclone.web.Application.__init__(self, handlers, **settings)
+
+if __name__ == '__main__':
+
+    config = {
+        'frontDoorArduino': "http://space:9080/",
+        'doorChangePost' : 'http://bang.bigasterisk.com:9069/inputChange',
+        'servePort' : 9081,
+        }
+
+    lcdParts = LcdParts(config['frontDoorArduino'] + 'lcd')
+    
+    log.startLogging(sys.stdout)
+    reactor.listenTCP(config['servePort'], Application(lcdParts))
+    reactor.run()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/frontDoorMessage/index.html	Mon Aug 01 03:30:30 2011 -0700
@@ -0,0 +1,45 @@
+<?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>front door message</title>
+    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
+  </head>
+  <body>
+
+    Front door message:
+    <div><textarea name="message"></textarea></div>
+    <div><input type="text" name="lastLine"/></div>
+    <div id="save"></div>
+<script type="text/javascript">
+// <![CDATA[
+
+$(function () { 
+
+    function setup(elem, url) {
+	$.get(url, function (data) { elem.val(data) });
+	
+	elem.keyup(function() {
+	    $("#save").css("color", "yellow").text("saving...");
+	    $.ajax({
+		type: "PUT",
+		url: url, 
+		data: elem.val(), 
+		success: function () {
+		    $("#save").css("color", "black").text("ok");
+		}
+	    });
+	});
+    }
+
+    setup($("textarea[name=message]"), "message");
+    setup($("input[name=lastLine]"), "lastLine");
+
+});
+
+// ]]>
+</script>
+
+  </body>
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/garageArduino/garage/garage.pde	Mon Aug 01 03:30:30 2011 -0700
@@ -0,0 +1,68 @@
+ void setup()   {                
+  
+  pinMode(2, INPUT);
+  digitalWrite(2, LOW); 
+// PIR sensor on here is a 
+// http://octopart.com/555-28027-parallax-708653 in a 
+// http://octopart.com/1551ggy-hammond-15686 box
+  
+  // the phototransistor on analog2 is jameco 2006414
+
+  Serial.begin(115200);
+}
+
+int newBlinks = 0;
+int lastLevel = 0;
+int threshold = 750;
+int hold = 3; // pulse must last this many loops. Guessing-- I don't know the loop rate or the pulse width
+int seenFor = 0;
+
+void loop()                     
+{
+  unsigned char head, cmd, arg;
+  int level = analogRead(3) < threshold;
+  
+  if (level) {
+     seenFor++; 
+     if (seenFor == hold) {
+        newBlinks++; 
+     }
+  } else {
+     seenFor = 0;
+  }
+
+  if (Serial.available() >= 3) {
+    head = Serial.read();
+    if (head != 0x60) {
+      Serial.flush();
+      return;
+    }
+    cmd = Serial.read();
+    arg = Serial.read();
+    Serial.flush();
+    if (cmd == 0x00) {
+      Serial.print("{\"ok\":true}\n");
+    } else if (cmd == 0x01) { // poll
+      Serial.print("{\"newBlinks\":");
+      Serial.print(newBlinks);
+      Serial.print(", \"motion\":");
+      Serial.print(digitalRead(2) ? "true" : "false");
+      Serial.print("}\n");
+      newBlinks = 0;
+    } else if (cmd == 0x02) {
+      // current level
+      Serial.print("{\"z\":");
+      Serial.print(analogRead(3));
+      Serial.print("}\n");
+    } else if (cmd == 0x03) {
+      if (arg != 0) {
+        threshold = arg << 2;
+      }
+      Serial.print("{\"threshold\":");
+      Serial.print(threshold);
+      Serial.print("}\n");
+    }
+  }
+}
+
+	
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/garageArduino/garageArduino.py	Mon Aug 01 03:30:30 2011 -0700
@@ -0,0 +1,227 @@
+#!bin/python
+"""
+talks to frontdoordriver.pde on an arduino
+"""
+
+from __future__ import division
+
+import cyclone.web, json, traceback, os, sys, time
+from twisted.python import log
+from twisted.internet import reactor, task
+from twisted.web.client import getPage
+sys.path.append("/my/proj/house/frontdoor")
+from loggingserial import LoggingSerial        
+sys.path.append("../../../room")
+from carbondata import CarbonClient
+sys.path.append("/my/site/magma")
+from stategraph import StateGraph      
+from rdflib import Namespace, RDF, Literal
+sys.path.append("/my/proj/homeauto/lib")
+from cycloneerr import PrettyErrorHandler
+
+ROOM = Namespace("http://projects.bigasterisk.com/room/")
+DEV = Namespace("http://projects.bigasterisk.com/device/")
+
+class ArduinoGarage(object):
+    def __init__(self, port='/dev/ttyACM0'):
+        self.ser = LoggingSerial(port=port, baudrate=115200, timeout=1)
+        self.ser.flush()
+
+    def ping(self):
+        self.ser.write("\x60\x00\x00")
+        msg = self.ser.readJson()
+        assert msg == {"ok":True}, msg
+
+    def poll(self):
+        self.ser.write("\x60\x01\x00")
+        ret = self.ser.readJson()
+        return ret
+
+    def lastLevel(self):
+        self.ser.write("\x60\x02\x00")
+        return self.ser.readJson()['z']
+
+    def setThreshold(self, t):
+        """set 10-bit threshold"""
+        self.ser.write("\x60\x03"+chr(max(1 << 2, t) >> 2))
+        return self.ser.readJson()['threshold']
+
+
+class Index(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        """
+        this is an acceptable status check since it makes a round-trip
+        to the arduino before returning success
+        """
+        self.settings.arduino.ping()
+        
+        self.set_header("Content-Type", "application/xhtml+xml")
+        self.write(open("index.html").read())
+
+class GraphPage(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        self.set_header("Content-Type", "application/x-trig")
+        g = StateGraph(ROOM['garageArduino'])
+        self.settings.poller.assertIsCurrent()
+        g.add((DEV['frontDoorMotion'], ROOM['state'],
+               ROOM['motion'] if self.settings.poller.lastValues['motion'] else
+               ROOM['noMotion']))
+        g.add((ROOM['house'], ROOM['usingPower'],
+               Literal(self.settings.poller.lastWatts, datatype=ROOM["#watts"])))
+        self.write(g.asTrig())
+
+class FrontDoorMotion(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        self.set_header("Content-Type", "application/javascript")
+        self.settings.poller.assertIsCurrent()
+        self.write(json.dumps({"frontDoorMotion" :
+                               self.settings.poller.lastValues['motion']}))
+
+class HousePower(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        self.set_header("Content-Type", "application/javascript")
+        self.settings.poller.assertIsCurrent()
+        w = self.settings.poller
+        self.write(json.dumps({
+            "currentWatts" : round(w.lastWatts, 2) if isinstance(w.lastWatts, float) else w.lastWatts,
+            "lastPulseAgo" : "%.1f sec ago" % (time.time() - w.lastBlinkTime) if w.lastBlinkTime is not None else "unknown",
+            "kwhPerBlink" : w.kwhPerBlink}))
+
+class HousePowerRaw(PrettyErrorHandler, cyclone.web.RequestHandler):
+    """
+    raw data from the analog sensor, for plotting or picking a noise threshold
+    """
+    def get(self):
+        self.set_header("Content-Type", "application/javascript")
+        self.settings.poller.assertIsCurrent()
+        self.write(json.dumps({"irLevels" : [[t1, lev1], [t2,lev2], ]}))
+
+class HousePowerThreshold(PrettyErrorHandler, cyclone.web.RequestHandler):
+    """
+    the level that's between between an IR pulse and the noise
+    """
+    def get(self):
+        self.set_header("Content-Type", "application/javascript")
+        self.settings.poller.assertIsCurrent()
+        self.write(json.dumps({"threshold" : thr}))
+        
+    def put(self):
+        pass
+
+
+class Application(cyclone.web.Application):
+    def __init__(self, ard, poller):
+        handlers = [
+            (r"/", Index),
+            (r"/graph", GraphPage),
+            (r"/frontDoorMotion", FrontDoorMotion),
+            (r'/housePower', HousePower),
+            (r'/housepower/raw', HousePowerRaw),
+            (r'/housepower/threshold', HousePowerThreshold),
+        ]
+        settings = {"arduino" : ard, "poller" : poller}
+        cyclone.web.Application.__init__(self, handlers, **settings)
+
+
+class Poller(object):
+    """
+    times the blinks to estimate power usage. Captures the other
+    returned sensor values too in self.lastValues
+    """
+    def __init__(self, ard, period):
+        self.ard = ard
+        self.period = period
+        self.carbon = CarbonClient(serverHost='bang')
+        self.lastBlinkTime = None
+        self.lastValues = None
+        self.lastPollTime = 0
+        self.lastWatts = "(just restarted; wait no data yet)"
+        self.kwhPerBlink = 1.0 # unsure
+        self.lastMotion = False
+
+    def assertIsCurrent(self):
+        """raise an error if the poll data is not fresh"""
+        dt = time.time() - self.lastPollTime
+        if dt > period * 2:
+            raise ValueError("last poll time was too old: %.1f sec ago" % dt)
+    
+    def poll(self):
+        now = time.time()
+        try:
+            try:
+                newData = ard.poll()
+            except ValueError, e:
+                print e
+            else:
+                self.lastPollTime = now
+                self.lastValues = newData # for other data besides the blinks
+                self.processBlinks(now, newData['newBlinks'])
+                self.processMotion(newData['motion'])
+            
+        except (IOError, OSError):
+            os.abort()
+        except Exception, e:
+            print "poll error", e
+            traceback.print_exc()
+
+    def processBlinks(self, now, b):
+        if b > 0:
+            if b > 1:
+                # todo: if it's like 1,1,2,2,2,2,1,1 then we
+                # need to subdivide those inner sample periods
+                # since there might really be two blinks. But
+                # if it's like 0,0,0,2,0,0, that should be
+                # treated like b=1 since it's probably noise
+                pass
+
+            if self.lastBlinkTime is not None:
+                dt = now - self.lastBlinkTime
+                dth = dt / 3600.
+                watts = self.kwhPerBlink / dth
+
+                if watts > 10000:
+                    # this pulse (or the previous one) is
+                    # likely noise. Too late for the previous
+                    # one, but we're going to skip this one
+                    return
+                else:
+                    self.lastWatts = watts
+
+                    # todo: remove this; a separate logger shall do it
+                    self.carbon.send('system.house.powerMeter_w', watts, now)
+
+            self.lastBlinkTime = now
+
+    def processMotion(self, state):
+        if state == self.lastMotion:
+            return
+        self.lastMotion = state
+        msg = json.dumps(dict(board='garage', 
+                              name="frontDoorMotion", state=state))
+        getPage('http://bang.bigasterisk.com:9069/inputChange',
+                method="POST",
+                postdata=msg,
+                headers={'Content-Type' : 'application/json'}
+                ).addErrback(self.reportError, msg)
+
+    def reportError(self, msg, *args):
+        print "post error", msg, args
+
+if __name__ == '__main__':
+
+    config = { # to be read from a file
+        'arduinoPort': '/dev/ttyACM0',
+        'servePort' : 9050,
+        'pollFrequency' : 5,
+        'boardName' : 'garage', # gets sent with updates
+        }
+
+    #log.startLogging(sys.stdout)
+
+    ard = ArduinoGarage()
+
+    period = 1/config['pollFrequency']
+    p = Poller(ard, period)
+    task.LoopingCall(p.poll).start(period)
+    reactor.listenTCP(config['servePort'], Application(ard, p))
+    reactor.run()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/garageArduino/index.html	Mon Aug 01 03:30:30 2011 -0700
@@ -0,0 +1,46 @@
+<?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>garageArduino</title>
+    <link rel="alternate" type="application/x-trig" title="RDF graph" href="graph" />
+    <style type="text/css" media="all">
+      /* <![CDATA[ */
+.val {
+font-weight: bold;
+} 
+/* ]]> */
+    </style>
+
+  </head>
+  <body>
+
+    <h1>garageArduino service</h1>
+
+    <p>Talking to an arduino uno on host <tt>slash</tt></p>
+
+    <p><a href="http://octopart.com/555-28027-parallax-708653">PIR sensor</a> (in <a href="http://octopart.com/1551ggy-hammond-15686">a box</a>) measuring front door motion: <span class="val" id="frontDoorMotion"/></p>
+
+    <p><a href="http://www.jameco.com/webapp/wcs/stores/servlet/ProductDisplay?langId=-1&amp;productId=2006414&amp;catalogId=10001&amp;freeText=2006414&amp;app.products.maxperpage=15&amp;storeId=10001&amp;search_type=jamecoall&amp;ddkey=http:StoreCatalogDrillDownView">phototransistor</a> watching IR pulses on the power meter: last pulse was <span class="val" id="lastPulseAgo"/>; current power usage is <span class="val"><span id="currentWatts"/> watts</span> (assuming <span class="val" id="kwhPerBlink"/> kwh/blink)</p>
+
+    <p><button type="submit" id="refresh">refresh</button></p>
+
+    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"/>
+    <script type="text/javascript">
+     // <![CDATA[
+    $(function () {
+	function setTexts(data) {
+	  $.each(data, function (k,v) { $("#"+k).text(v); })
+	}
+	function refresh() {
+	    $.getJSON("frontDoorMotion", setTexts);
+	    $.getJSON("housePower", setTexts);
+	}
+	refresh();
+	$("#refresh").click(refresh);
+    });
+     // ]]>
+    </script>
+  </body>
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/tomatoWifi/table.mustache	Mon Aug 01 03:30:30 2011 -0700
@@ -0,0 +1,28 @@
+<?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">
+<div xmlns="http://www.w3.org/1999/xhtml" class="wifiNow">
+
+  <table><tr> 
+
+    <th class="name">Name</th> 
+    <th class="lease">Lease</th> 
+    <th class="ip">IP address</th> 
+    <th class="rssi">dBm</th> 
+    <th class="mac">MAC address</th> 
+    <th class="router">Router</th> 
+  </tr>
+  {{#rows}}
+  <tr class="{{cls}}">
+    <td>{{name}}</td>
+    <td class="lease">{{lease}}</td>
+    <td class="ip">{{ip}}</td>
+    <td class="rssi">{{signal}}</td>
+    <td class="mac">{{mac}}</td>
+    <td>{{router}}</td>
+  </tr>
+  {{/rows}}
+  </table>
+
+
+</div>
\ No newline at end of file
--- /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()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/tomatoWifi/wifi.py	Mon Aug 01 03:30:30 2011 -0700
@@ -0,0 +1,112 @@
+import re, ast, restkit, logging, socket
+from rdflib import Literal
+
+log = logging.getLogger()
+
+class Wifi(object):
+    """
+    gather the users of wifi from the tomato routers
+    """
+    def __init__(self, tomatoConfig="/my/site/magma/tomato_config.js",
+                 accessN3="/my/proj/openid_proxy/access.n3"):
+
+        # ideally this would all be in the same rdf store, with int and
+        # ext versions of urls
+
+        txt = open(tomatoConfig).read().replace('\n', '')
+        self.knownMacAddr = jsValue(txt, 'knownMacAddr')
+        tomatoUrl = jsValue(txt, 'tomatoUrl')
+
+        from rdflib.Graph import Graph
+        g = Graph()
+        g.parse(accessN3, format="n3")
+        repl = {'/tomato1/' : None, '/tomato2/' : None}
+        for k in repl:
+            rows = list(g.query('''
+            PREFIX p: <http://bigasterisk.com/openid_proxy#>
+            SELECT ?prefix WHERE {
+              [
+                p:requestPrefix ?public;
+                p:proxyUrlPrefix ?prefix
+                ]
+            }''', initBindings={"public" : Literal(k)}))
+            repl[k] = str(rows[0][0])
+
+        self.routers = []
+        for url in tomatoUrl:
+            name = url
+            for k, v in repl.items():
+                url = url.replace(k, v)
+
+            r = restkit.Resource(url, timeout=2)
+            r.name = {'tomato1' : 'bigasterisk3',
+                      'tomato2' : 'bigasterisk4'}[name.split('/')[1]]
+            self.routers.append(r)
+
+
+    def getPresentMacAddrs(self):
+        aboutIp = {}
+        byMac = {} # mac : [ip]
+
+        for router in self.routers:
+            log.debug("GET %s", router)
+            try:
+                data = router.get().body_string()
+            except socket.error:
+                log.warn("get on %s failed" % router)
+                continue
+
+            for (ip, mac, iface) in jsValue(data, 'arplist'):
+                aboutIp.setdefault(ip, {}).update(dict(
+                    ip=ip,
+                    router=router.name,
+                    mac=mac,
+                    iface=iface,
+                    ))
+            
+                byMac.setdefault(mac, set()).add(ip)
+
+            for (name, ip, mac, lease) in jsValue(data, 'dhcpd_lease'):
+                if lease.startswith('0 days, '):
+                    lease = lease[len('0 days, '):]
+                aboutIp.setdefault(ip, {}).update(dict(
+                    router=router.name,
+                    rawName=name,
+                    mac=mac,
+                    lease=lease
+                    ))
+                
+                byMac.setdefault(mac, set()).add(ip)
+                
+            for iface, mac, signal in jsValue(data, 'wldev'):
+                matched = False
+                for addr in aboutIp.values():
+                    if (addr['router'], addr['mac']) == (router.name, mac):
+                        addr.update(dict(signal=signal, iface=iface))
+                        matched = True
+                if not matched:
+                    aboutIp["mac-%s-%s" % (router, mac)] = dict(
+                        router=router.name,
+                        mac=mac,
+                        signal=signal,
+                        )
+
+        ret = []
+        for addr in aboutIp.values():
+            if addr.get('ip') in ['192.168.1.1', '192.168.1.2', '192.168.0.2']:
+                continue
+            try:
+                addr['name'] = self.knownMacAddr[addr['mac']]
+            except KeyError:
+                addr['name'] = addr.get('rawName')
+                if addr['name'] in [None, '*']:
+                    addr['name'] = 'unknown'
+            ret.append(addr)
+
+        return ret
+
+
+def jsValue(js, variableName):
+    # using literal_eval instead of json parser to handle the trailing commas
+    val = re.search(variableName + r'\s*=\s*(.*?);', js, re.DOTALL).group(1)
+    return ast.literal_eval(val)