view MonitorDev.py @ 14:de698afbe6fc

Avoid using pylab helper functions to reduce the magic level.
author darius
date Tue, 23 Oct 2007 01:01:48 +0000
parents 9d5b291cfd01
children f1832dec26e3
line wrap: on
line source

#!/usr/bin/env python

############################################################################
# Monitoring/control interface to hardware for beermon
#
# $Id: MonitorDev.py,v 1.2 2007/09/29 14:51:20 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 threading, re, time, pexpect

class MonitorDev(threading.Thread):
    """This class continually polls the hardware for temperature
readings and accepts state changes to heat & cool"""

    # 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('> ')

    # 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, _log, conf):
        """_log is a logging object, conf is a ConfigParser object"""
        global log
        log = _log
        threading.Thread.__init__(self)

        # Collect parameters
        self.coolRelay = conf.getint('hardware', 'coolRelay')
        self.heatRelay = conf.getint('hardware', 'heatRelay')
        self.fermenterId = conf.get('hardware', 'fermenterId')
        self.fridgeId = conf.get('hardware', 'fridgeId')
        self.ambientId = conf.get('hardware', 'ambientId')
        self.minCoolOnTime = conf.getfloat('hardware', 'minCoolOnTime')
        self.minCoolOffTime = conf.getfloat('hardware', 'minCoolOffTime')
        self.minHeatOnTime = conf.getfloat('hardware', 'minHeatOnTime')
        self.minHeatOffTime = conf.getfloat('hardware', 'minHeatOffTime')
        self.minHeatOvershoot = conf.getfloat('hardware', 'minHeatOvershoot')
        self.minCoolOvershoot = conf.getfloat('hardware', 'minCoolOvershoot')

        # Setup locking & spawn SSH
        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()

        # Search for 1-wire modules
        self.devs = self.find1wire()
        self.tempdevs = filter(self.istemp, self.devs)

        log.debug("fermenterId - %s" % (self.fermenterId))
        log.debug("fridgeId - %s" % (self.fridgeId))
        log.debug("ambientId - %s" % (self.ambientId))
        log.debug("minCoolOnTime - %d, minCoolOffTime - %d" %
                  (self.minCoolOnTime, self.minCoolOffTime))
        log.debug("minHeatOnTime - %d, minHeatOffTime - %d" %
                  (self.minHeatOnTime, self.minHeatOffTime))
        log.debug("minHeatOvershoot - %3.2f, minCoolOvershoot - %3.2f" %
                  (self.minHeatOvershoot, self.minCoolOvershoot))
        self.start()
        
    def setspeed(self):
        """Set the speed microcom talks to the serial port to 38400"""
        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):
        """Scan the bus for 1-wire devices"""
        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):
        """Returns true if the 1-wire device is a temperature sensor"""
        [family, a, b, c, d, e, f, g] = id.split(':')
        if (family == '10'):
            return True
        else:
            return False

    def updateTemps(self):
        """Update our cached copy of temperatures"""
        for i in self.tempdevs:
            try:
                self.temps[i] = float(self.readTemp(i))
                self.lastUpdate[i] = time.time()
            except OWReadError:
                # Ignore this - just results in no update reflected by lastUpdate
                pass
            
        return(self.temps)

    def readTemp(self, id):
        """Read the temperature of a sensor"""
        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 OWReadError

        return(line)

    def setState(self, state):
        """Set the heat/cool state, track the on/off time"""
        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 run(self):
        """Sit in a loop polling temperatures"""
        while True:
            self.updateTemps()