changeset 0:1c6f5a0281c7

Initial revision
author darius
date Sun, 23 Sep 2007 03:17:47 +0000
parents
children c0b01c8c63eb
files beermon.py
diffstat 1 files changed, 291 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/beermon.py	Sun Sep 23 03:17:47 2007 +0000
@@ -0,0 +1,291 @@
+#!/usr/bin/env python
+
+import pexpect, re, threading, time, logging
+from logging.handlers import RotatingFileHandler
+
+class ROMReadError(Exception):
+    pass
+
+class Control():
+    targetTemp = 18
+    hysteresis = 4
+    pollInterval = 30
+    
+    def __init__(self, m):
+        self.m = m
+        self.initLog()
+        
+    def initLog(self):
+        # Init our logging
+        global log
+        log = logging.getLogger("monitor")
+
+        # Default to warts and all logging
+        log.setLevel(logging.DEBUG)
+
+        # Log to this file
+        logfile = logging.handlers.RotatingFileHandler(filename = "/tmp/monitor.log",
+                                                       maxBytes = 10000, backupCount = 3)
+
+        # And stderr
+        logstderr = logging.StreamHandler()
+
+        # Format it nicely
+        formatter = logging.Formatter(fmt = "%(asctime)s: %(message)s", datefmt = "%Y%m%d %H:%M:%S")
+
+        # Glue it all together
+        logfile.setFormatter(formatter)
+        logstderr.setFormatter(formatter)
+        log.addHandler(logfile)
+        log.addHandler(logstderr)
+        return(log)
+
+    def doit(self):
+        log.debug("=== Inited ===")
+        log.debug("target temperature - %3.2f" % (self.targetTemp))
+        log.debug("fermenterId - %s" % (self.m.fermenterId))
+        log.debug("fridgeId - %s" % (self.m.fridgeId))
+        log.debug("ambientId - %s" % (self.m.ambientId))
+        log.debug("minCoolOnTime - %d, minCoolOffTime - %d" % (self.m.minCoolOnTime, self.m.minCoolOffTime))
+        log.debug("minHeatOnTime - %d, minHeatOffTime - %d" % (self.m.minHeatOnTime, self.m.minHeatOffTime))
+        log.debug("pollInterval - %d" % (self.pollInterval))
+        
+        log.debug("=== Starting ===")
+        log.debug("Fermenter	Fridge	Ambient	State	New State")
+        while True:
+            if (self.m.lastUpdate == 0):
+                print "%s Invalid data" % (time.asctime())
+                time.sleep(30)
+                self.m.setState('idle')
+                continue
+
+            nextState = "-"
+
+            diff = self.m.temps[self.m.fermenterId] - self.targetTemp
+            if (self.m.currState == 'idle'):
+                # If we're idle then only heat or cool if the temperate difference is out of the
+                # hysteresis range
+                if (abs(diff) > self.hysteresis):
+                    if (diff < 0 and self.m.minHeatOffTime + self.m.lastHeatOff < time.time()):
+                        nextState = 'heat'
+                    elif (diff > 0 and self.m.minHeatOffTime + self.m.lastHeatOff < time.time()):
+                        nextState = 'cool'
+            elif (self.m.currState == 'cool'):
+                # Go idle as soon as we can, there will be overshoot anyway
+                if (diff < 0 and self.m.minCoolOnTime + self.m.lastCoolOn < time.time()):
+                    nextState = 'idle'
+            elif (self.m.currState == 'heat'):
+                if (diff > 0 and self.m.minHeatOnTime + self.m.lastHeatOn < time.time()):
+                    nextState = 'idle'
+            else:
+                raise KeyError
+
+            log.debug("%3.2f   	%3.2f	%3.2f	%s	%s" %
+                      (self.m.temps[self.m.fermenterId],
+                       self.m.temps[self.m.fridgeId], self.m.temps[self.m.ambientId],
+                       self.m.currState, nextState))
+            if (nextState != "-"):
+                self.m.setState(nextState)
+                
+            time.sleep(self.pollInterval)
+            
+
+class MonitorDev(threading.Thread):
+    # Match a ROM ID (eg 00:11:22:33:44:55:66:77)
+    romre = re.compile('([0-9a-f]{2}:){7}[0-9a-f]{2}')
+    # Match the prompt
+    promptre = re.compile('> ')
+
+    coolRelay = 7
+    heatRelay = 6
+    
+    fermenterId = '10:eb:48:21:01:08:00:df'
+    fridgeId = '10:a6:2a:c4:00:08:00:11'
+    ambientId = '10:97:1b:fe:00:08:00:d1'
+    
+    # minimum time the cooler must spend on/off
+    minCoolOnTime = 10 * 60
+    minCoolOffTime = 10 * 60
+
+    # minimum time the heater must spend on/off
+    minHeatOnTime = 60
+    minHeatOffTime = 60
+
+    temps = {}
+    lastUpdate = 0
+
+    # Lock to gate access to the comms
+    commsLock = None
+
+    currState = 'idle'
+
+    lastHeatOn = 0
+    lastHeatOff = 0
+    lastCoolOn = 0
+    lastCoolOff = 0
+
+    def __init__(self):
+        threading.Thread.__init__(self)
+        self.commsLock = threading.Lock()
+        self.p = pexpect.spawn('/usr/bin/ssh', ['-xt', '-enone', '-i', '/home/darius/.ssh/id_wrt', 'root@wrt', '(echo logged in; microcom -D/dev/cua/1)'])
+        assert(self.p.expect('logged in') == 0)
+        self.p.timeout = 3
+        self.setspeed()
+        self.devs = self.find1wire()
+        self.temps = filter(self.istemp, self.devs)
+
+        self.start()
+        
+    def setspeed(self):
+        self.commsLock.acquire()
+        self.p.send('~')
+        assert(self.p.expect('t - set terminal') == 0)
+        self.p.send('t')
+        assert(self.p.expect('p - set speed') == 0)
+        self.p.send('p')
+        assert(self.p.expect('f - 38400') == 0)
+        self.p.send('f')
+        assert(self.p.expect('done!') == 0)
+        self.commsLock.release()
+        
+    def find1wire(self):
+        self.commsLock.acquire()
+        self.p.sendline('')
+        assert(self.p.expect('> ') == 0)
+        self.p.sendline('sr')
+        # Echo
+        assert(self.p.expect('sr') == 0)
+
+        # Send a new line which will give us a command prompt to stop on
+        # later. We could use read() but that would make the code a lot
+        # uglier
+        self.p.sendline('')
+
+        devlist = []
+
+        # Loop until we get the command prompt (> ) collecting ROM IDs
+        while True:
+            idx = self.p.expect([self.romre, self.promptre])
+            if (idx == 0):
+                # Matched a ROM
+                #print "Found ROM " + self.p.match.group()
+                devlist.append(self.p.match.group(0))
+            elif (idx == 1):
+                # Matched prompt, exit
+                break
+            else:
+                # Unpossible!
+                self.commsLock.release()
+                raise SystemError()
+
+        self.commsLock.release()
+
+        return(devlist)
+
+    def istemp(self, id):
+        [family, a, b, c, d, e, f, g] = id.split(':')
+        if (family == '10'):
+            return True
+        else:
+            return False
+
+    def updateTemps(self):
+        tmp = {}
+        for i in self.temps:
+            tmp[i] = float(self.readTemp(i))
+
+        self.temps = tmp
+        self.lastUpdate = time.time()
+        return(self.temps)
+
+    def readTemp(self, id):
+        self.commsLock.acquire()
+        cmd = 'te ' + id
+        self.p.sendline(cmd)
+        # Echo
+        assert(self.p.expect(cmd) == 0)
+        # Eat EOL left from expect
+        self.p.readline()
+
+        line = self.p.readline().strip()
+        self.commsLock.release()
+        # 'CRC mismatch' can mean that we picked the wrong ROM..
+        if (re.match('CRC mismatch', line) != None):
+            raise ROMReadError
+
+        return(line)
+
+    def setState(self, state):
+        if (state == 'cool'):
+            relay = 1 << self.coolRelay
+        elif (state == 'heat'):
+            relay = 1 << self.heatRelay
+        elif (state == 'idle'):
+            relay = 0
+        else:
+            raise(ValueError)
+
+        if (state == self.currState):
+            return
+
+        # Keep track of when we last turned off or on
+        if (state == 'cool'):
+            if (self.currState == 'heat'):
+                self.lastHeatOff = time.time()
+            self.lastCoolOn = time.time()
+        elif (state == 'heat'):
+            if (self.currState == 'cool'):
+                self.lastCoolOff = time.time()
+            self.lastHeatOn = time.time()
+        else:
+            if (self.currState == 'cool'):
+                self.lastCoolOff = time.time()
+            if (self.currState == 'heat'):
+                self.lastHeatOff = time.time()
+
+        self.currState = state
+        
+        self.commsLock.acquire()
+        # Need the extra spaces cause the parser in the micro is busted
+        cmd = 'out c    %02x' % relay
+        self.p.sendline(cmd)
+            # Echo
+        assert(self.p.expect(cmd) == 0)
+        self.commsLock.release()
+        
+    def polltemps(self, temps):
+        while True:
+            for d in temps:
+                #print d
+                t = gettemp(p, d)
+                print "%s -> %s" % (d, t)
+            print
+
+    def run(self):
+        while True:
+            self.updateTemps()
+
+def main():
+    import time, monitor
+
+    m = monitor.MonitorDev()
+
+    try:
+        c = monitor.Control(m)
+        # Wait for the first temperature readings to come through, saves
+        # getting an 'invalid data' message
+        time.sleep(3)
+        c.doit()
+        log.debug("doit exited")
+
+    except KeyboardInterrupt:
+        log.debug("Exiting due to keyboard interrupt")
+        
+    finally:
+        # Make sure we try and turn it off if something goes wrong
+        m.setState('idle')
+
+    sys.exit(0)
+    
+if __name__ == "__main__":  
+    main()