changeset 4:be855a111619

move a bunch of services into this tree, give them all web status pages Ignore-this: a11e90f9d2cd9470565c743f54943c4b
author drewp@bigasterisk.com
date Mon, 08 Aug 2011 00:31:31 -0700
parents 6c2e83a0c85b
children 4c44c80a6a72
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&amp;productId=2006414&amp;catalogId=10001&amp;freeText=2006414&amp;app.products.maxperpage=15&amp;storeId=10001&amp;search_type=jamecoall&amp;ddkey=http:StoreCatalogDrillDownView">phototransistor</a> watching IR pulses on the power meter: last pulse was <span class="val" id="lastPulseAgo"/>; current power usage is <span class="val"><span id="currentWatts"/> watts</span> (assuming <span class="val" id="kwhPerBlink"/> kwh/blink)</p>
 
+    <p>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()