# HG changeset patch # User darius # Date 1190517467 0 # Node ID af88c0f96295728adfe476fda31a8d91e86198e4 # Parent 650701a185c9d8baa976fbbbd0cc4d4f38e163d9 Initial revision diff -r 650701a185c9 -r af88c0f96295 beermon.py --- /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()