Mercurial > code > home > repos > homeauto
changeset 809:bebb8f7c5a3e
move a bunch of services into this tree, give them all web status pages
Ignore-this: a11e90f9d2cd9470565c743f54943c4b
darcs-hash:20110808073131-312f9-a7f420d66388cedae458276d672a27a9249f1e2f.gz
author | drewp <drewp@bigasterisk.com> |
---|---|
date | Mon, 08 Aug 2011 00:31:31 -0700 |
parents | 867f59c83dba |
children | 44e1ca03ddf1 |
files | lib/cycloneerr.py service/bluetooth/bluetoothService.py service/garageArduino/garageArduino.py service/garageArduino/index.html service/onewire/onewire.py service/powerInverter/powerInverter.py service/sba/sba.py service/speechMusic/readme service/speechMusic/speechMusic.py service/starArduino/starArduino.py service/theaterArduino/index.html service/theaterArduino/theaterArduino.py |
diffstat | 12 files changed, 911 insertions(+), 104 deletions(-) [+] |
line wrap: on
line diff
--- /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 "<html><title>%(code)d: %(message)s</title>" \ + "<body>%(code)d: %(message)s<pre>%(tb)s</pre></body></html>" % { + "code": status_code, + "message": httplib.responses[status_code], + "tb" : cgi.escape(tb), + }
--- 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()
--- 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)
--- 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 @@ } /* ]]> */ </style> - </head> <body> @@ -24,9 +23,13 @@ <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>Recent raw IR sensor data: <div><img src="" id="raw"/></div></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 () { @@ -36,6 +39,33 @@ function refresh() { $.getJSON("frontDoorMotion", setTexts); $.getJSON("housePower", setTexts); + + $.getJSON("housePower/raw", function (data) { + var xp=[], yp=[]; + var start = data.irLevels[0][0]; + var maxTime = 0; + $.each(data.irLevels, function (i, xy) { + maxTime = xy[0] - start; + xp.push(maxTime.toPrecision(3)); + yp.push(xy[1]); + }); + // edit with http://imagecharteditor.appspot.com/ + $("#raw").attr("src", + "http://chart.apis.google.com/chart"+ + "?chxr=0,0,"+maxTime+"|1,0,1024"+ + "&chds=0,"+maxTime+",0,1024"+ + "&chxl=2:|seconds"+ + "&chxs=0,676767,11.5,0,lt,676767|1,676767,11.5,-0.5,l,676767"+ + "&chxt=x,y,x"+ + "&chs=600x200"+ + "&cht=lxy"+ + "&chco=76A4FB"+ + "&chd=t:"+xp.join(",")+"|"+yp.join(",")+ + "&chg=10,20"+ + "&chls=2"+ + "&chma=40,20,20,30"+ + "&chm=h,FF0000,0,0.7:50:2,1"); + }); } refresh(); $("#refresh").click(refresh);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/onewire/onewire.py Mon Aug 08 00:31:31 2011 -0700 @@ -0,0 +1,194 @@ +#!/usr/bin/python +""" +normal accessing of the 'temperature' field on the sensors wasn't +working. I always got '85' (the power-on reset value). owfs verison is 2.7p2 + +http://sourceforge.net/mailarchive/forum.php?thread_name=fba87cb90612051724o705bfed0ub780325b915ed541%40mail.gmail.com&forum_name=owfs-developers + +Asking for simultaneous read seems to work, and I'm fine with doing that. + +the stock modules for onewire are bad; they will take all your CPU. to +turn them off, see: +http://tomasz.korwel.net/2006/07/02/owfs-instalation-on-ubuntu-606/#comment-12246 + +For the python 'ow' package, get +http://downloads.sourceforge.net/owfs/owfs-2.7p7.tar.gz?modtime=1222687523&big_mirror=0 +or similar. Install the libusb-dev and swig packages first for usb and +python support. + +2009-02-21 i'm now on ow.__version__ = '2.7p16-1.15' +./configure --disable-owtcl --disable-owperl --disable-owphp --disable-ha7 --disable-ownet --disable-ownetlib --disable-owserver --disable-parport + +2011-02-26 now on 2.8p6 + +how to run their server: +bang(pts/6):/my/dl/lib/owfs-2.8p6/module/owserver/src/c% sudo ./owserver -u -p 9999 --foreground --error_level 9 --error_print 2 + +owshell/src/c/owget -s 9999 /uncached/10.52790F020800/temperature /uncached/10.4F718D000800/temperature /uncached/10.9AA2BE000800/temperature + + +but the previous 2.7 version was getting 2/3 measurements, while 2.8 +was getting 1/3 measurements! + +""" +from __future__ import division +import time, logging, traceback, sys, cyclone.web, jsonlib, restkit +from twisted.internet.task import LoopingCall, deferLater +from twisted.internet.defer import inlineCallbacks, returnValue +from twisted.internet import reactor +import ow + +sys.path.append("/my/proj/homeauto/lib") +from cycloneerr import PrettyErrorHandler +from logsetup import log + +sys.path.append("/my/proj/room") +from carbondata import CarbonClient + +class TempReader(object): + def __init__(self): + self.expectedSensors = 3 + self.ow = None + self.initOnewire() + self.firstSensorLoop = True + + def initOnewire(self): + """open usb connection and configure ow lib. Run this again if + things get corrupt""" + ow.init('u') + # this might PRINT a 'Could not open the USB adapter.' message, but I + # don't know how to trap it + + @inlineCallbacks + def getCompleteTemps(self, maxTime=120): + ret = {} + tries = 0 + now = time.time() + giveUp = now + maxTime + + self.requestTemps() + sensors = set(self.allSensors()) + + while now < giveUp: + tries += 1 + ret.update(self.getTemps(sensors - set(ret.keys()))) + + if len(ret) >= self.expectedSensors: + log.info("after %s tries, temps=%s" % (tries, ret)) + break + + log.debug("..only have %s measurements; still trying for %d secs" % + (len(ret), giveUp - now)) + self.initOnewire() + self.requestTemps() + yield deferLater(reactor, .5, lambda: None) + now = time.time() + else: + log.info("giving up after %s secs, only got %s measurements" % + (maxTime, len(ret))) + returnValue(dict([(s.address,val) for s, val in ret.items()])) + + def allSensors(self): + return ow.Sensor('/').sensors() + + def requestTemps(self): + ow.owfs_put('/uncached/simultaneous/temperature', '1') + + def getTemps(self, sensors): + ret = {} + try: + for sens in sensors: + if self.firstSensorLoop: + log.debug("found sensor address %r, type=%r" % + (sens.address, sens.type)) + if sens.type != 'DS18S20': + continue + try: + t = sens.temperature.strip() + if t == '85': + log.debug( + " sensor %s says 85 (C), power-on reset value" % + sens.address) + continue + tFar = float(t) * 9/5 + 32 + log.debug(" %s reports temp %r F" % (sens.address, tFar)) + except ow.exUnknownSensor, e: + log.warn(e) + continue + ret[sens] = tFar + except KeyboardInterrupt: raise + except Exception, e: + traceback.print_exc() + self.firstSensorLoop = False + return ret + + +class Poller(object): + def __init__(self): + self.reader = TempReader() + self.lastPollTime = 0 + self.lastDoc = [] + self.carbon = CarbonClient(serverHost='bang') + + def getHttpTemps(self): + ret = {} + + for url, name in [("http://star:9014/", "ariroom"), + ("http://space:9080/", "frontDoor"), + ]: + for tries in range(3): + try: + res = restkit.Resource(url, timeout=5) + temp = jsonlib.read(res.get("temperature").body_string(), + use_float=True)['temp'] + log.debug("got http temp %s = %r", name, temp) + ret[name] = temp + break + except Exception, e: + log.warn(e) + return ret + + @inlineCallbacks + def sendTemps(self): + try: + temps = yield self.reader.getCompleteTemps(maxTime=30) + except Exception, e: + reactor.stop() + raise + temps.update(self.getHttpTemps()) + now = time.time() + rows = [] + for k, v in temps.items(): + row = 'system.house.temp.%s' % { + '104F718D00080038': 'downstairs' , + '109AA2BE000800C7': 'livingRoom', + '1052790F02080086' : 'bedroom', + '1014958D0008002B': 'unused1', # when you set this, fix expectedSensors count too + '10CB6CBE0008005E': 'bedroom-broken', + }.get(str(k), str(k)), float(v) + self.carbon.send(row[0], row[1], now) + rows.append(row) + + self.lastPollTime = now + self.lastDoc = rows + +class Index(PrettyErrorHandler, cyclone.web.RequestHandler): + def get(self): + + dt = time.time() - self.settings.poller.lastPollTime + if dt > 120 + 50: + raise ValueError("last poll %s sec ago" % dt) + + self.set_header("Content-Type", "text/plain") + self.write("onewire reader (also gathers temps from arduinos); logs to graphite.\n\n Last temps: %r" % self.settings.poller.lastDoc) + +if __name__ == '__main__': + log.setLevel(logging.DEBUG) + poller = Poller() + poller.sendTemps() + reactor.listenTCP(9078, cyclone.web.Application([ + (r'/', Index), + ], poller=poller)) + + LoopingCall(poller.sendTemps).start(interval=120) + reactor.run()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/powerInverter/powerInverter.py Mon Aug 08 00:31:31 2011 -0700 @@ -0,0 +1,115 @@ +#!/usr/bin/python + +import struct, time, sys, logging, traceback, os, cyclone.web +from serial import Serial, SerialException +from twisted.internet.task import LoopingCall +from twisted.internet import reactor + +sys.path.append("/my/proj/homeauto/lib") +from cycloneerr import PrettyErrorHandler +from logsetup import log + +sys.path.append("/my/proj/room") +from carbondata import CarbonClient + +class Command(object): + getVersion = 0x01 + getPowerNow = 0x10 + +class Sleeping(ValueError): + """haven't checked the spec, but this is what I get when the + inverter is off at night""" + +class Comm(object): + def __init__(self, device): + self.ser = Serial(device, baudrate=19200) + + def request(self, device, number, command): + self.writeMsg(device, number, command, data="\x01") + return self.readMsg() + + def requestNumeric(self, device, number, command): + buf = self.request(device, number, command) + if len(buf) != 3: + if buf == '\x10\x05': + raise Sleeping() + raise ValueError("Expected 3 bytes for command %r, not %r" % + (command, buf)) + msb, lsb, exp = struct.unpack("!BBb", buf) + return ((msb << 8) + lsb) * 10**exp + + def writeMsg(self, device, number, command, data=""): + length = len(data) + commandVal = getattr(Command, command) + checksum = sum(x for x in [length, device, number, commandVal] + + [ord(x) for x in data]) & 0xff + args = 0x80, 0x80, 0x80, length, device, number, commandVal, data, checksum + log.debug("Sending: %s %r", command, args) + self.ser.write(struct.pack("!BBBBBBBsB", *args)) + + + def readMsg(self): + log.debug("Read header..") + (s1, s2, s3, length, device, number, command) = struct.unpack( + "!BBBBBBB", self.ser.read(7)) + if not s1 == s2 == s3 == 0x80: + raise ValueError("incorrect header in response: %r" % vars()) + log.debug("Read %d bytes of data", length) + data = self.ser.read(length) + cksum = self.ser.read(1) + log.debug(" -> %r", data) + return data + +class Poller(object): + def __init__(self, carbon): + self.carbon = carbon + self.lastPollTime = 0 + self.reset() + LoopingCall(self.poll).start(interval=10) + + def reset(self): + log.info("reopening serial port") + for port in ['/dev/ttyUSB0', '/dev/ttyUSB1']: + try: + self.comm = Comm(port) + break + except SerialException, e: + pass + else: + # among other things, a serial exception for too many open files + log.error(e) + os.abort() + log.info("version: %r", self.comm.request(device=1, number=0, + command="getVersion")) + + def poll(self): + try: + watts = self.comm.requestNumeric(device=1, number=0, + command="getPowerNow") + self.carbon.send('system.house.solar.power_w', watts) + except Sleeping: + log.debug("sleeping") + except ValueError: + log.error(traceback.format_exc()) + self.reset() + except Exception: + traceback.print_exc() + os.abort() + self.lastPollTime = time.time() + + +class Index(PrettyErrorHandler, cyclone.web.RequestHandler): + def get(self): + age = time.time() - self.settings.poller.lastPollTime + if age > 12: + raise ValueError("poll data is stale. age=%s" % age) + + self.write("powerInverter reading from serial port and writing to graphite") + +log.setLevel(logging.INFO) +carbon = CarbonClient(serverHost='bang') +p = Poller(carbon) +reactor.listenTCP(9078, cyclone.web.Application([ + (r'/', Index), + ], poller=p)) +reactor.run()
--- a/service/sba/sba.py Sun Aug 07 21:50:21 2011 -0700 +++ b/service/sba/sba.py Mon Aug 08 00:31:31 2011 -0700 @@ -15,6 +15,9 @@ log.msg(str(self.s.__dict__)) self.sendControl() + def ping(self): + pass # waiting for spec + def sendControl(self): controlBits = [0, 1, 0, 0, 0, @@ -93,6 +96,7 @@ class IndexHandler(cyclone.web.RequestHandler): def get(self): + self.settings.chain.ping() self.set_header("Content-type", "text/html") self.write(open("sba.html").read())
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/speechMusic/readme Mon Aug 08 00:31:31 2011 -0700 @@ -0,0 +1,1 @@ +this is meant to be run in multiple rooms with various output device configs
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/speechMusic/speechMusic.py Mon Aug 08 00:31:31 2011 -0700 @@ -0,0 +1,124 @@ +#!bin/python + +""" +play sounds according to POST requests. cooperate with pubsubhubbub +""" +import web, sys, jsonlib, subprocess, os, tempfile, logging +from subprocess import check_call +sys.path.append("/my/proj/csigen") +from generator import tts +import xml.etree.ElementTree as ET +logging.basicConfig(level=logging.INFO, format="%(created)f %(asctime)s %(levelname)s %(message)s") +log = logging.getLogger() + +sensorWords = {"wifi" : "why fi", + "bluetooth" : "bluetooth"} + +def aplay(device, filename): + paDeviceName = { + 'garage' : 'alsa_output.pci-0000_01_07.0.analog-stereo', + 'living' : 'alsa_output.pci-0000_00_04.0.analog-stereo', + }[device] + subprocess.call(['paplay', + '-d', paDeviceName, + filename]) + +def soundOut(preSound=None, speech='', postSound=None, fast=False): + + speechWav = tempfile.NamedTemporaryFile(suffix='.wav') + + root = ET.Element("SABLE") + r = ET.SubElement(root, "RATE", + attrib=dict(SPEED="+50%" if fast else "+0%")) + for sentence in speech.split('.'): + div = ET.SubElement(r, "DIV") + div.set("TYPE", "sentence") + div.text = sentence + + sounds = [] + delays = [] + + if preSound is not None: + sounds.append(preSound) + delays.extend([0,0]) # assume stereo + + speechSecs = tts(root, speechWav.name) + sounds.append(speechWav.name) + delays.append(.4) + if postSound is not None: + sounds.append(postSound) + delays.extend([speechSecs + .4]*2) # assume stereo + + if len(sounds) == 1: + outName = sounds[0] + else: + outWav = tempfile.NamedTemporaryFile(suffix='.wav') + check_call(['/usr/bin/sox', '--norm', '--combine', 'merge', + ]+sounds+[ + outWav.name, + 'delay', ]+map(str, delays)+[ + 'channels', '1']) + outName = outWav.name + + aplay('living', outName) + +class visitorNet(object): + def POST(self): + data = jsonlib.loads(web.data()) + + if data.get('action') == 'arrive': + + snd = ('/my/music/entrance/%s.wav' % + data['name'].replace(' ', '_').replace(':', '_')) + if not os.path.exists(snd): + snd = None + + soundOut(preSound="/my/music/snd/angel_ogg/angel_question.wav", + # sic: + speech="Neew %s. %s" % (sensorWords[data['sensor']], + data['name']), + postSound=snd, fast=True) + return 'ok' + + if data.get('action') == 'leave': + soundOut(preSound='/my/music/entrance/leave.wav', + speech="lost %s. %s" % (sensorWords[data['sensor']], + data['name']), + fast=True) + return 'ok' + + return "nothing to do" + +class index(object): + def GET(self): + web.header('Content-type', 'text/html') + return ''' +<p><form action="speak" method="post">say: <input type="text" name="say"> <input type="submit"></form></p> +<p><form action="testSound" method="post"> <input type="submit" value="test sound"></form></p> +''' + +class speak(object): + def POST(self): + txt = web.input()['say'] + log.info("speak: %r", txt) + soundOut(preSound='/my/music/snd/Oxygen/KDE-Im-Highlight-Msg-44100.wav', + speech=txt) + return "sent" + +class testSound(object): + def POST(self): + soundOut(preSound='/my/music/entrance/leave.wav') + return 'ok' + +urls = ( + r'/', 'index', + r'/speak', 'speak', + r'/testSound', 'testSound', + r'/visitorNet', 'visitorNet', + ) + +app = web.application(urls, globals(), autoreload=True) + +if __name__ == '__main__': + sys.argv.append("9049") + app.run()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/starArduino/starArduino.py Mon Aug 08 00:31:31 2011 -0700 @@ -0,0 +1,66 @@ +""" +arduino driver for the nightlight+buttons+temp setup running on star + +""" +from __future__ import division + +import sys, jsonlib +from twisted.internet import reactor, task +import cyclone.web + +sys.path.append("/my/proj/pixel/shiftweb") +from drvarduino import ShiftbriteArduino +from shiftweb import hexFromRgb, rgbFromHex + +sys.path.append("/my/proj/homeauto/lib") +from cycloneerr import PrettyErrorHandler +from logsetup import log + +sys.path.append("/my/proj/ariremote") +from oscserver import ArduinoWatcher + +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 Temperature(PrettyErrorHandler, cyclone.web.RequestHandler): + def get(self): + f = self.settings.arduino.getTemperature() + self.set_header("Content-Type", "application/json") + self.write(jsonlib.write({"temp" : f})) + +class Brite(PrettyErrorHandler, cyclone.web.RequestHandler): + def get(self, pos): + self.set_header("Content-Type", "text/plain") + self.write(hexFromRgb(self.settings.colors[int(pos)])) + + def put(self, pos): + channel = int(pos) + colors = self.settings.colors + colors[channel] = rgbFromHex(self.request.body) + self.settings.arduino.update(colors) + self.set_header("Content-Type", "text/plain") + self.write("updated %r" % colors) + +class Graph(PrettyErrorHandler, cyclone.web.RequestHandler): + def get(self): + raise NotImplementedError + +if __name__ == '__main__': + sb = ShiftbriteArduino(numChannels=3) + + colors = [(0,0,0)] * sb.numChannels + + aw = ArduinoWatcher(sb) + task.LoopingCall(aw.poll).start(1.0/20) + + reactor.listenTCP(9014, cyclone.web.Application([ + (r'/', Index), + (r'/temperature', Temperature), + (r'/brite/(\d+)', Brite), + (r'/graph', Graph), + ], arduino=sb, colors=colors)) + reactor.run()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/theaterArduino/index.html Mon Aug 08 00:31:31 2011 -0700 @@ -0,0 +1,160 @@ +<?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></title> + </head> + <body> + + <h1>pyduino web interface</h1> + + <p>Use GET or PUT on the resources below. The value is a "0" or "1" + string. PUT "output" to /pin/d2/mode (etc) to make it writable.</p> + + <div> + pin/d2 : <input type="checkbox" name="d2" value="set" id="d2"/> + <span class="mode"> <input type="radio" name="d2-mode" value="input" id="d2-input"/> <label for="d2-input">input</label> + <input type="radio" name="d2-mode" value="output" id="d2-output"/> <label for="d2-output">output</label> </span> + </div> + + <div> + pin/d3 : <input type="checkbox" name="d3" value="set" id="d3"/> + <span class="mode"> <input type="radio" name="d3-mode" value="input" id="d3-input"/> <label for="d3-input">input</label> + <input type="radio" name="d3-mode" value="output" id="d3-output"/> <label for="d3-output">output</label> </span> + </div> + + <div> + pin/d4 : <input type="checkbox" name="d4" value="set" id="d4"/> + <span class="mode"> <input type="radio" name="d4-mode" value="input" id="d4-input"/> <label for="d4-input">input</label> + <input type="radio" name="d4-mode" value="output" id="d4-output"/> <label for="d4-output">output</label> </span> + </div> + + <div> + pin/d5 : <input type="checkbox" name="d5" value="set" id="d5"/> + <span class="mode"> <input type="radio" name="d5-mode" value="input" id="d5-input"/> <label for="d5-input">input</label> + <input type="radio" name="d5-mode" value="output" id="d5-output"/> <label for="d5-output">output</label> </span> + </div> + + <div> + pin/d6 : <input type="checkbox" name="d6" value="set" id="d6"/> + <span class="mode"> <input type="radio" name="d6-mode" value="input" id="d6-input"/> <label for="d6-input">input</label> + <input type="radio" name="d6-mode" value="output" id="d6-output"/> <label for="d6-output">output</label> </span> + </div> + + <div> + pin/d7 : <input type="checkbox" name="d7" value="set" id="d7"/> + <span class="mode"> <input type="radio" name="d7-mode" value="input" id="d7-input"/> <label for="d7-input">input</label> + <input type="radio" name="d7-mode" value="output" id="d7-output"/> <label for="d7-output">output</label> </span> + </div> + + <div> + pin/d8 : <input type="checkbox" name="d8" value="set" id="d8"/> + <span class="mode"> <input type="radio" name="d8-mode" value="input" id="d8-input"/> <label for="d8-input">input</label> + <input type="radio" name="d8-mode" value="output" id="d8-output"/> <label for="d8-output">output</label> </span> + </div> + + <div> + pin/d9 : <input type="checkbox" name="d9" value="set" id="d9"/> + <span class="mode"> <input type="radio" name="d9-mode" value="input" id="d9-input"/> <label for="d9-input">input</label> + <input type="radio" name="d9-mode" value="output" id="d9-output"/> <label for="d9-output">output</label> </span> + </div> + + <div> + pin/d10 : <input type="checkbox" name="d10" value="set" id="d10"/> + <span class="mode"> <input type="radio" name="d10-mode" value="input" id="d10-input"/> <label for="d10-input">input</label> + <input type="radio" name="d10-mode" value="output" id="d10-output"/> <label for="d10-output">output</label> </span> + </div> + + <div> + pin/d11 : <input type="checkbox" name="d11" value="set" id="d11"/> + <span class="mode"> <input type="radio" name="d11-mode" value="input" id="d11-input"/> <label for="d11-input">input</label> + <input type="radio" name="d11-mode" value="output" id="d11-output"/> <label for="d11-output">output</label> </span> + </div> + + <div> + pin/d12 : <input type="checkbox" name="d12" value="set" id="d12"/> + <span class="mode"> <input type="radio" name="d12-mode" value="input" id="d12-input"/> <label for="d12-input">input</label> + <input type="radio" name="d12-mode" value="output" id="d12-output"/> <label for="d12-output">output</label> </span> + </div> + + <div> + pin/d13 : <input type="checkbox" name="d13" value="set" id="d13"/> + <span class="mode"> <input type="radio" name="d13-mode" value="input" id="d13-input"/> <label for="d13-input">input</label> + <input type="radio" name="d13-mode" value="output" id="d13-output"/> <label for="d13-output">output</label> </span> + </div> + + <div><button id="refresh">Refresh</button> <input type="checkbox" name="autoRefresh" id="autoRefresh"/> <label for="autoRefresh">Auto refresh</label></div> + + + <div id="ajaxError"/> + + <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.js"></script> + <script type="text/javascript"> + // <![CDATA[ + $(function() { + $("#ajaxError").ajaxError(function (ev, xhr) { + $(this).text("Error: " + xhr.responseText); + }); + + function refresh() { + $(".mode input").css('opacity', .2); + $("input[value=set]").css('opacity', .2) + .each(function (i, inp) { + var id = $(inp).attr('name'); + $.ajax({ + url: "pin/" + id, + type: "GET", + success: function (data, textStatus, xhr) { + $(inp).css('opacity', 1).val(data == "1" ? ["set"] : []); + }, + }); + $.ajax({ + url: "pin/" + id + "/mode", + type: "GET", + success: function (data, textStatus, xhr) { + var match = $("#" + id + "-" + data); + match.parent().find("input").css('opacity', 1); + match.click(); + } + }); + }); + } + + function refreshLoop() { + if ($("#autoRefresh").is(":checked")) { + refresh(); + setTimeout(function() { refreshLoop(); }, + 500); // refresh is async, so these could pile up + } + } + + $("#autoRefresh").click(refreshLoop); + $("#refresh").click(refresh); + + $(".mode input").removeAttr('disabled').change(function () { + var id = $(this).attr('id').replace(/-.*/, ""); + $.ajax({ + type: "PUT", + contentType: "text/plain", + url: "pin/" + id + "/mode", + data: $(this).val(), + }); + }); + + $("input[value=set]").change(function () { + var id = $(this).attr('id'); + $.ajax({ + type: "PUT", + contentType: "text/plain", + url: "pin/"+id, + data: $("#"+id+":checked").val() ? "1" : "0", + }); + }); + refresh(); + }); + // ]]> +</script> + + </body> +</html>
--- /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()