Mercurial > ~darius > hgwebdir.cgi > beermon.old
view beermon.py @ 6:a51f78d44552
- Reduce hystersis down to 0.5C
- Properly track stale data (hopefully anyway :)
- Check if the monitor thread has died (ie unexpected exception)
- Re-work some variable names to be clearer (& add comments)
author | darius |
---|---|
date | Fri, 28 Sep 2007 13:05:11 +0000 |
parents | dba51b33fd9e |
children | 860936fab75f |
line wrap: on
line source
#!/usr/bin/env python ############################################################################ # Monitor & control fermenter temperature # v1.0 # # $Id: beermon.py,v 1.5 2007/09/28 13:05:11 darius Exp $ # # Depends on: Python 2.3 (I think) # ############################################################################ # # Copyright (C) 2007 Daniel O'Connor. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # ############################################################################ import pexpect, re, threading, time, logging, sys, traceback from logging.handlers import RotatingFileHandler class ROMReadError(Exception): pass class ThreadDied(Exception): pass class Control(): targetTemp = 18 hysteresis = 0.5 pollInterval = 30 staleDataTime = 30 def __init__(self, m, _log): self.m = m global log log = _log self.cv = threading.Condition() self.cv.acquire() def doit(self): 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: # Check if our monitor thread has died if (not self.m.isAlive()): raise ThreadDied, "Monitor thread has died" # Check for stale data if (self.m.lastUpdate[self.m.fermenterId] + self.staleDataTime < time.time()): log.debug("Stale data") self.cv.wait(self.pollInterval) self.m.setState('idle') continue # Work out what state we should go into 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 band 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'): # Ditto if (diff > 0 and self.m.minHeatOnTime + self.m.lastHeatOn < time.time()): nextState = 'idle' else: # Not possible.. 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) self.cv.wait(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 # Dictionary of sensor IDs & temperatures temps = {} # Dictionary of sensor IDs & epoch times lastUpdate = {} # List of all device IDs devs = [] # List of temperature sensor IDs tempdevs = [] # 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.tempdevs = 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): for i in self.tempdevs: try: self.temps[i] = float(self.readTemp(i)) self.lastUpdate[i] = time.time() except ROMReadError: # Ignore this - just results in no update reflected by lastUpdate pass 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 initLog(): # Init our logging log = logging.getLogger("monitor") # Default to warts and all logging log.setLevel(logging.DEBUG) # Log to this file logfile = logging.handlers.RotatingFileHandler(filename = "/tmp/beermon.log", maxBytes = 1000000, 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 main(): import beermon global log log = initLog() log.debug("=== Initing ===") log.debug("$Id: beermon.py,v 1.5 2007/09/28 13:05:11 darius Exp $") m = None exitCode = 0 try: m = beermon.MonitorDev() c = beermon.Control(m, log) # 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") except Exception, e: log.debug("Something went wrong, details below:") log.debug(e) log.debug(reduce(lambda x, y: x + y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback))) exitCode = 1 finally: # Make sure we try and turn it off if something goes wrong if (m != None): m.setState('idle') sys.exit(exitCode) if __name__ == "__main__": main()