# HG changeset patch # User drewp # Date 1312788691 25200 # Node ID bebb8f7c5a3e36369803d174bf87e8c8732d6d17 # Parent 867f59c83dba4d426535a93b3f7a71008939db5b move a bunch of services into this tree, give them all web status pages Ignore-this: a11e90f9d2cd9470565c743f54943c4b darcs-hash:20110808073131-312f9-a7f420d66388cedae458276d672a27a9249f1e2f.gz diff -r 867f59c83dba -r bebb8f7c5a3e lib/cycloneerr.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/cycloneerr.py Mon Aug 08 00:31:31 2011 -0700 @@ -0,0 +1,17 @@ +import httplib, cgi + +class PrettyErrorHandler(object): + """ + mix-in to improve cyclone.web.RequestHandler + """ + def get_error_html(self, status_code, **kwargs): + try: + tb = kwargs['exception'].getTraceback() + except AttributeError: + tb = "" + return "%(code)d: %(message)s" \ + "%(code)d: %(message)s
%(tb)s
" % { + "code": status_code, + "message": httplib.responses[status_code], + "tb" : cgi.escape(tb), + } diff -r 867f59c83dba -r bebb8f7c5a3e service/bluetooth/bluetoothService.py --- a/service/bluetooth/bluetoothService.py Sun Aug 07 21:50:21 2011 -0700 +++ b/service/bluetooth/bluetoothService.py Mon Aug 08 00:31:31 2011 -0700 @@ -16,10 +16,11 @@ """ from __future__ import absolute_import import logging, time, datetime, restkit, jsonlib, cyclone.web, sys -from bluetooth import DeviceDiscoverer -from twisted.internet import reactor, defer, task +from bluetooth import discover_devices, lookup_name +from twisted.internet import reactor, task +from twisted.internet.threads import deferToThread from rdflib.Graph import Graph -from rdflib import Literal, Variable, Namespace +from rdflib import Literal, Namespace, RDFS, URIRef from pymongo import Connection from dateutil import tz @@ -31,61 +32,15 @@ ROOM = Namespace("http://projects.bigasterisk.com/room/") -class Disco(DeviceDiscoverer): - # it might be cool if this somehow returned - # _bt.EVT_INQUIRY_RESULT_WITH_RSSI: results. see - # /usr/share/pycentral/python-bluez/site-packages/bluetooth.py - def device_discovered(self, address, device_class, name): - log.debug("seeing: %s - %s (class 0x%X)" % (address, name, device_class)) - self.nearby.append((address, name)) - - def inquiry_complete(self): - pass - - def process_inquiry(self): - # more async version of the normal method - """ - Starts calling process_event, returning a deferred that fires - when we're done. - """ - self.done_inquiry = defer.Deferred() - - if self.is_inquiring or len(self.names_to_find) > 0: - self.keep_processing() - else: - self.done_inquiry.callback(None) - - return self.done_inquiry +def getNearbyDevices(): + addrs = discover_devices() - def keep_processing(self): - # this one still blocks "a little bit" - if self.is_inquiring or len(self.names_to_find) > 0: - reactor.callLater(0, self.keep_processing) - log.debug("process_event()") - self.process_event() # <-- blocks here - else: - self.done_inquiry.callback(None) + # this can be done during discover_devices, but my plan was to + # cache it more in here + names = dict((a, lookup_name(a)) for a in addrs) + log.debug("discover found %r %r", addrs, names) + return addrs, names - def nearbyDevices(self): - """deferred to list of (addr,name) pairs""" - self.nearby = [] - self.find_devices() - d = self.process_inquiry() - d.addCallback(lambda result: self.nearby) - return d - -def devicesFromAddress(address): - for row in graph.query( - "SELECT ?dev { ?dev rm:bluetoothAddress ?addr }", - initNs=dict(rm=ROOM), - initBindings={Variable("?addr") : Literal(address)}): - (dev,) = row - yield dev - -graph = Graph() -graph.parse("phones.n3", format="n3") - -d = Disco() hub = restkit.Resource( # PSHB not working yet; "http://bang:9030/" "http://slash:9049/" @@ -102,74 +57,66 @@ msg['created'] = datetime.datetime.now(tz.gettz('UTC')) mongo.insert(msg, safe=True) +def deviceUri(addr): + return URIRef("http://bigasterisk.com/bluetooth/%s" % addr) + class Poller(object): def __init__(self): - self.lastDevs = set() # addresses - self.lastNameForAddress = {} + self.lastAddrs = set() # addresses self.currentGraph = Graph() self.lastPollTime = 0 def poll(self): log.debug("get devices") - devs = d.nearbyDevices() + devs = deferToThread(getNearbyDevices) devs.addCallback(self.compare) devs.addErrback(log.error) return devs - def compare(self, newDevs): + def compare(self, (addrs, names)): self.lastPollTime = time.time() - log.debug("got: %r", newDevs) - lostDevs = self.lastDevs.copy() - prevDevs = self.lastDevs.copy() - self.lastDevs.clear() - stmts = [] - - for address, name in newDevs: - stmts.append((ROOM['bluetooth'], - ROOM['senses'], - Literal(str(address)))) - if address not in prevDevs: - matches = 0 - for dev in devicesFromAddress(address): - log.info("found %s" % dev) - matches += 1 - if not matches: - log.info("no matches for %s (%s)" % (name, address)) - print "%s %s %s" % (time.time(), name, address) - - self.lastNameForAddress[address] = name - print 'mongoInsert', ({"sensor" : "bluetooth", - "address" : address, - "name" : name, - "action" : "arrive"}) + newGraph = Graph() + addrs = set(addrs) + for addr in addrs.difference(self.lastAddrs): + self.recordAction('arrive', addr, names) + for addr in self.lastAddrs.difference(addrs): + self.recordAction('leave', addr, names) + for addr in addrs: + uri = deviceUri(addr) + newGraph.add((ROOM['bluetooth'], ROOM['senses'], uri)) + if addr in names: + newGraph.add((uri, RDFS.label, Literal(names[addr]))) + self.lastAddrs = addrs + self.currentGraph = newGraph - lostDevs.discard(address) - self.lastDevs.add(address) - - for address in lostDevs: - print 'mongoInsert', ({"sensor" : "bluetooth", - "address" : address, - "name" : self.lastNameForAddress[address], - "action" : "leave"}) - - for dev in devicesFromAddress(address): - log.info("lost %s" % dev) + def recordAction(self, action, addr, names): + doc = {"sensor" : "bluetooth", + "address" : addr, + "action" : action} + if addr in names: + doc["name"] = names[addr] + log.info("action: %s", doc) + mongoInsert(doc) class Index(PrettyErrorHandler, cyclone.web.RequestHandler): def get(self): age = time.time() - self.settings.poller.lastPollTime - if age > 60 + 30: + if age > self.settings.config['period'] + 30: raise ValueError("poll data is stale. age=%s" % age) self.write("bluetooth watcher. ") if __name__ == '__main__': - log.setLevel(logging.DEBUG) + config = { + "period" : 60, + } + log.setLevel(logging.INFO) poller = Poller() reactor.listenTCP(9077, cyclone.web.Application([ (r'/', Index), - ], poller=poller)) - task.LoopingCall(poller.poll).start(1) + # graph, json, table, ... + ], poller=poller, config=config)) + task.LoopingCall(poller.poll).start(config['period']) reactor.run() diff -r 867f59c83dba -r bebb8f7c5a3e service/garageArduino/garageArduino.py --- a/service/garageArduino/garageArduino.py Sun Aug 07 21:50:21 2011 -0700 +++ b/service/garageArduino/garageArduino.py Mon Aug 08 00:31:31 2011 -0700 @@ -93,8 +93,12 @@ """ def get(self): self.set_header("Content-Type", "application/javascript") - self.settings.poller.assertIsCurrent() - self.write(json.dumps({"irLevels" : [[t1, lev1], [t2,lev2], ]})) + pts = [] + for i in range(60): + level = self.settings.arduino.lastLevel() + pts.append((round(time.time(), 3), level)) + + self.write(json.dumps({"irLevels" : pts})) class HousePowerThreshold(PrettyErrorHandler, cyclone.web.RequestHandler): """ @@ -116,8 +120,8 @@ (r"/graph", GraphPage), (r"/frontDoorMotion", FrontDoorMotion), (r'/housePower', HousePower), - (r'/housepower/raw', HousePowerRaw), - (r'/housepower/threshold', HousePowerThreshold), + (r'/housePower/raw', HousePowerRaw), + (r'/housePower/threshold', HousePowerThreshold), ] settings = {"arduino" : ard, "poller" : poller} cyclone.web.Application.__init__(self, handlers, **settings) diff -r 867f59c83dba -r bebb8f7c5a3e service/garageArduino/index.html --- a/service/garageArduino/index.html Sun Aug 07 21:50:21 2011 -0700 +++ b/service/garageArduino/index.html Mon Aug 08 00:31:31 2011 -0700 @@ -12,7 +12,6 @@ } /* ]]> */ - @@ -24,9 +23,13 @@

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

+

Recent raw IR sensor data:

+

+ + + + diff -r 867f59c83dba -r bebb8f7c5a3e service/theaterArduino/theaterArduino.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/theaterArduino/theaterArduino.py Mon Aug 08 00:31:31 2011 -0700 @@ -0,0 +1,145 @@ +""" +arduino example sketches, 'StandardFirmata'. + +####easy_install http://github.com/lupeke/python-firmata/tarball/master + +Now using http://code.google.com/p/pyduino, modified to run at 57600 +baud like my arduino's code does. pyduino is better than the lupeke +one in that you can read your settings off the output pins + +Note that there are some startup delays and you may not hear about +input changes for a few seconds. +""" +from __future__ import division +import sys, cyclone.web, time, simplejson, os +from twisted.web.client import getPage +from twisted.internet import reactor, task + +sys.path.append("/my/proj/homeauto/lib") +from cycloneerr import PrettyErrorHandler +from logsetup import log + +sys.path.append("pyduino-read-only") +import pyduino + +def _num(name): + if name.startswith('d'): + return int(name[1:]) + raise ValueError(name) + +class pin(PrettyErrorHandler, cyclone.web.RequestHandler): + def get(self, name): + self.set_header("Content-Type", "text/plain") + arduino = self.settings.arduino + arduino.iterate() + self.write(str(int(arduino.digital[_num(name)].read()))) + + def put(self, name): + t1 = time.time() + self.settings.arduino.digital[_num(name)].write(int(self.request.body)) + log.debug("arduino write in %.1f ms" % (1000 * (time.time() - t1))) + + +class pinMode(PrettyErrorHandler, cyclone.web.RequestHandler): + def get(self, name): + self.set_header("Content-Type", "text/plain") + mode = self.settings.arduino.digital[_num(name)].get_mode() + self.write({pyduino.DIGITAL_INPUT : "input", + pyduino.DIGITAL_OUTPUT : "output"}[mode]) + + def put(self, name): + mode = { + "input" : pyduino.DIGITAL_INPUT, + "output" : pyduino.DIGITAL_OUTPUT}[self.request.body.strip()] + self.settings.arduino.digital[_num(name)].set_mode(mode) + +class Pid(PrettyErrorHandler, cyclone.web.RequestHandler): + def get(self): + self.set_header("Content-Type", "text/plain") + self.write(str(os.getpid())) + +class index(PrettyErrorHandler, cyclone.web.RequestHandler): + def get(self): + """ + this is a suitable status check; it does a round-trip to arduino + """ + # this would be a good ping() call for pyduino + self.settings.arduino.sp.write(chr(pyduino.REPORT_VERSION)) + self.settings.arduino.iterate() + + self.set_header("Content-Type", "application/xhtml+xml") + self.write(open('index.html').read()) + +class Application(cyclone.web.Application): + def __init__(self, arduino): + handlers = [ + (r"/", index), + (r'/pin/(.*)/mode', pinMode), + (r'/pin/(.*)', pin), + (r'/pid', Pid), + # web refresh could benefit a lot from a json resource that + # gives all the state + ] + settings = {"arduino" : arduino,} + cyclone.web.Application.__init__(self, handlers, **settings) + +class WatchPins(object): + def __init__(self, arduino, conf): + self.arduino, self.conf = arduino, conf + self.lastState = {} + self.pins = conf['watchPins'] + if self.pins == 'allInput': + self.watchAllInputs() + for pin in self.pins: + arduino.digital_ports[pin >> 3].set_active(1) + arduino.digital[pin].set_mode(pyduino.DIGITAL_INPUT) + + def watchAllInputs(self): + raise NotImplementedError("this needs to be updated whenever the modes change") + self.pins = [p for p in range(2, 13+1) if + self.arduino.digital[p].get_mode() == + pyduino.DIGITAL_INPUT] + + def reportPostError(self, fail, pin, value, url): + log.error("failed to send pin %s update (now %s) to %r: %r" % (pin, value, url, fail)) + + def poll(self): + try: + self._poll() + except Exception, e: + log.error("during poll:", exc_info=1) + + def _poll(self): + # this can IndexError for a port number being out of + # range. I'm not sure how- maybe error data coming in the + # port? + arduino.iterate() + for pin in self.pins: + current = arduino.digital[pin].read() + if current != self.lastState.get(pin, None): + d = getPage( + self.conf['post'], + method="POST", + postdata=simplejson.dumps(dict(board=self.conf['boardName'], pin=pin, level=int(current))), + headers={'Content-Type' : 'application/json'}) + d.addErrback(self.reportPostError, pin, current, self.conf['post']) + + self.lastState[pin] = current + +if __name__ == '__main__': + + config = { # to be read from a file + 'arduinoPort': '/dev/ttyUSB0', + 'servePort' : 9056, + 'pollFrequency' : 20, + 'post' : 'http://bang:9069/pinChange', + 'boardName' : 'theater', # gets sent with updates + 'watchPins' : [9, 10], # or 'allInput' (not yet working) + # todo: need options to preset inputs/outputs at startup + } + + arduino = pyduino.Arduino(config['arduinoPort']) + wp = WatchPins(arduino, config) + task.LoopingCall(wp.poll).start(1/config['pollFrequency']) + reactor.listenTCP(config['servePort'], Application(arduino)) + reactor.run()