# HG changeset patch # User Drew Perttula # Date 1312194630 25200 # Node ID 6fd208b97616d0d421ae704334f2cda3f3847595 start Ignore-this: e06ac598970a0d4750f588ab89f56996 diff -r 000000000000 -r 6fd208b97616 service/deskArduino/desk/desk.pde --- /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 + + + + desk arduino + + + + + + + + +

desk arduino

+ +
+
+ + + +
+ Using the + Farbtastic color picker +
+ + + \ No newline at end of file diff -r 000000000000 -r 6fd208b97616 service/environment/environment.py --- /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: rdf') + +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() diff -r 000000000000 -r 6fd208b97616 service/frontDoorArduino/frontDoorArduino.py --- /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() diff -r 000000000000 -r 6fd208b97616 service/frontDoorArduino/index.html --- /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 @@ + + + + front door + + + + + + +
+

lcd

+
+ set message: +
+
+
+
+ + + + \ No newline at end of file diff -r 000000000000 -r 6fd208b97616 service/garageArduino/garage/garage.pde --- /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"); + } + } +} + + diff -r 000000000000 -r 6fd208b97616 service/garageArduino/garageArduino.py --- /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() diff -r 000000000000 -r 6fd208b97616 service/garageArduino/index.html --- /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 @@ + + + + + garageArduino + + + + + + +

garageArduino service

+ +

Talking to an arduino uno on host slash

+ +

PIR sensor (in a box) measuring front door motion:

+ +

phototransistor watching IR pulses on the power meter: last pulse was ; current power usage is watts (assuming kwh/blink)

+ +

+ + + + \ No newline at end of file diff -r 000000000000 -r 6fd208b97616 service/tomatoWifi/table.mustache --- /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 @@ + + +
+ + + + + + + + + + + {{#rows}} + + + + + + + + + {{/rows}} +
NameLeaseIP addressdBmMAC addressRouter
{{name}}{{lease}}{{ip}}{{signal}}{{mac}}{{router}}
+ + +
\ No newline at end of file diff -r 000000000000 -r 6fd208b97616 service/tomatoWifi/tomatoWifi.py --- /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() diff -r 000000000000 -r 6fd208b97616 service/tomatoWifi/wifi.py --- /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: + 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)