6
|
1 #!/usr/bin/env python
|
|
2
|
|
3 ############################################################################
|
|
4 # Control class for beermon
|
|
5 #
|
10
|
6 # $Id: Control.py,v 1.2 2007/09/29 14:51:20 darius Exp $
|
6
|
7 #
|
|
8 # Depends on: Python 2.3 (I think)
|
|
9 #
|
|
10 ############################################################################
|
|
11 #
|
|
12 # Copyright (C) 2007 Daniel O'Connor. All rights reserved.
|
|
13 #
|
|
14 # Redistribution and use in source and binary forms, with or without
|
|
15 # modification, are permitted provided that the following conditions
|
|
16 # are met:
|
|
17 # 1. Redistributions of source code must retain the above copyright
|
|
18 # notice, this list of conditions and the following disclaimer.
|
|
19 # 2. Redistributions in binary form must reproduce the above copyright
|
|
20 # notice, this list of conditions and the following disclaimer in the
|
|
21 # documentation and/or other materials provided with the distribution.
|
|
22 #
|
|
23 # THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
|
24 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
25 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
26 # ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
|
|
27 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
28 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
29 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
30 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
31 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
32 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
33 # SUCH DAMAGE.
|
|
34 #
|
|
35 ############################################################################
|
|
36
|
|
37 import threading, ConfigParser, time
|
|
38
|
|
39 class OWReadError(Exception):
|
|
40 """Raised when we failed to read from a 1-wire device, could be a timeout
|
|
41 or the device is non-existent, etc"""
|
|
42 pass
|
|
43
|
|
44 class ThreadDied(Exception):
|
|
45 """Raised when the monitor thread has died for some reason"""
|
|
46 pass
|
|
47
|
|
48 class Control():
|
|
49 """This class is responsible for controlling the temperature of
|
|
50 the fermenter by switching the heater & cooler."""
|
|
51 staleDataTime = 30
|
|
52
|
|
53 def __init__(self, _log, m, conf):
|
10
|
54 """m is a MonitorDev object, _log is a logging object, conf is a ConfigParser object"""
|
6
|
55 global log
|
|
56 log = _log
|
|
57
|
|
58 self.m = m
|
|
59
|
|
60 self.targetTemp = conf.getfloat('control', 'targetTemp')
|
|
61 self.hysteresis = conf.getfloat('control', 'hysteresis')
|
|
62 self.pollInterval = conf.getfloat('control', 'pollInterval')
|
|
63
|
|
64 log.debug("target temperature - %3.2f" % (self.targetTemp))
|
|
65 log.debug("hysteresis - %3.2f" % (self.hysteresis))
|
|
66 log.debug("pollInterval - %d" % (self.pollInterval))
|
|
67
|
10
|
68 # We sleep on this, using time.sleep() doesn't work well in a
|
|
69 # threaded environment (eg ctrl-c will be blocked until the
|
|
70 # timeout expires.)
|
6
|
71 self.cv = threading.Condition()
|
|
72 self.cv.acquire()
|
|
73
|
|
74 def doit(self):
|
|
75 """Runs forever controlling the temperature until something breaks or interrupted"""
|
|
76 log.debug("=== Starting ===")
|
|
77 log.debug("Fermenter Fridge Ambient State New State")
|
|
78 while True:
|
10
|
79 # Check if the monitor thread has died
|
6
|
80 if (not self.m.isAlive()):
|
|
81 raise ThreadDied, "Monitor thread has died"
|
|
82
|
|
83 # Check for stale data
|
|
84 if (self.m.lastUpdate[self.m.fermenterId] + self.staleDataTime < time.time()):
|
|
85 log.debug("Stale data")
|
|
86 self.cv.wait(self.pollInterval)
|
|
87 self.m.setState('idle')
|
|
88 continue
|
|
89
|
|
90 # Work out what state we should go into
|
|
91 nextState = "-"
|
|
92 # Temperature diff, -ve => too cold, +ve => too warm
|
|
93 diff = self.m.temps[self.m.fermenterId] - self.targetTemp
|
|
94 if (self.m.currState == 'idle'):
|
|
95 # If we're idle then only heat or cool if the temperate difference is out of the
|
|
96 # hysteresis band
|
|
97 if (abs(diff) > self.hysteresis):
|
|
98 if (diff < 0 and self.m.minHeatOffTime + self.m.lastHeatOff < time.time()):
|
|
99 nextState = 'heat'
|
|
100 elif (diff > 0 and self.m.minHeatOffTime + self.m.lastHeatOff < time.time()):
|
|
101 nextState = 'cool'
|
|
102 elif (self.m.currState == 'cool'):
|
|
103 # Work out if we should go idle (based on min on time & overshoot)
|
|
104 if (diff + self.m.minCoolOvershoot < 0 and self.m.minCoolOnTime + self.m.lastCoolOn < time.time()):
|
|
105 nextState = 'idle'
|
|
106 elif (self.m.currState == 'heat'):
|
|
107 # Ditto
|
|
108 if (diff - self.m.minHeatOvershoot > 0 and self.m.minHeatOnTime + self.m.lastHeatOn < time.time()):
|
|
109 nextState = 'idle'
|
|
110 else:
|
|
111 # Not possible..
|
|
112 raise KeyError
|
|
113
|
|
114 log.debug("%3.2f %3.2f %3.2f %s %s" %
|
|
115 (self.m.temps[self.m.fermenterId],
|
|
116 self.m.temps[self.m.fridgeId], self.m.temps[self.m.ambientId],
|
|
117 self.m.currState, nextState))
|
|
118
|
|
119 if (nextState != "-"):
|
|
120 self.m.setState(nextState)
|
|
121
|
|
122 self.cv.wait(self.pollInterval)
|
|
123
|
|
124
|