Mercurial > code > home > repos > homeauto
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&productId=2006414&catalogId=10001&freeText=2006414&app.products.maxperpage=15&storeId=10001&search_type=jamecoall&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)