6
|
1 #!/usr/bin/env python
|
|
2
|
|
3 ############################################################################
|
|
4 # Control class for beermon
|
|
5 #
|
15
|
6 # $Id: Control.py,v 1.4 2008/01/29 11:27: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 ThreadDied(Exception):
|
|
40 """Raised when the monitor thread has died for some reason"""
|
|
41 pass
|
|
42
|
|
43 class Control():
|
|
44 """This class is responsible for controlling the temperature of
|
|
45 the fermenter by switching the heater & cooler."""
|
|
46 staleDataTime = 30
|
|
47
|
|
48 def __init__(self, _log, m, conf):
|
10
|
49 """m is a MonitorDev object, _log is a logging object, conf is a ConfigParser object"""
|
6
|
50 global log
|
|
51 log = _log
|
|
52
|
|
53 self.m = m
|
|
54
|
|
55 self.targetTemp = conf.getfloat('control', 'targetTemp')
|
|
56 self.hysteresis = conf.getfloat('control', 'hysteresis')
|
|
57 self.pollInterval = conf.getfloat('control', 'pollInterval')
|
|
58
|
|
59 log.debug("target temperature - %3.2f" % (self.targetTemp))
|
|
60 log.debug("hysteresis - %3.2f" % (self.hysteresis))
|
|
61 log.debug("pollInterval - %d" % (self.pollInterval))
|
|
62
|
10
|
63 # We sleep on this, using time.sleep() doesn't work well in a
|
|
64 # threaded environment (eg ctrl-c will be blocked until the
|
|
65 # timeout expires.)
|
6
|
66 self.cv = threading.Condition()
|
|
67 self.cv.acquire()
|
|
68
|
|
69 def doit(self):
|
|
70 """Runs forever controlling the temperature until something breaks or interrupted"""
|
|
71 log.debug("=== Starting ===")
|
|
72 log.debug("Fermenter Fridge Ambient State New State")
|
15
|
73
|
6
|
74 while True:
|
10
|
75 # Check if the monitor thread has died
|
6
|
76 if (not self.m.isAlive()):
|
|
77 raise ThreadDied, "Monitor thread has died"
|
|
78
|
|
79 # Check for stale data
|
|
80 if (self.m.lastUpdate[self.m.fermenterId] + self.staleDataTime < time.time()):
|
|
81 log.debug("Stale data")
|
|
82 self.cv.wait(self.pollInterval)
|
|
83 self.m.setState('idle')
|
|
84 continue
|
|
85
|
|
86 # Work out what state we should go into
|
|
87 nextState = "-"
|
|
88 # Temperature diff, -ve => too cold, +ve => too warm
|
|
89 diff = self.m.temps[self.m.fermenterId] - self.targetTemp
|
|
90 if (self.m.currState == 'idle'):
|
|
91 # If we're idle then only heat or cool if the temperate difference is out of the
|
|
92 # hysteresis band
|
|
93 if (abs(diff) > self.hysteresis):
|
|
94 if (diff < 0 and self.m.minHeatOffTime + self.m.lastHeatOff < time.time()):
|
|
95 nextState = 'heat'
|
|
96 elif (diff > 0 and self.m.minHeatOffTime + self.m.lastHeatOff < time.time()):
|
|
97 nextState = 'cool'
|
|
98 elif (self.m.currState == 'cool'):
|
|
99 # Work out if we should go idle (based on min on time & overshoot)
|
|
100 if (diff + self.m.minCoolOvershoot < 0 and self.m.minCoolOnTime + self.m.lastCoolOn < time.time()):
|
|
101 nextState = 'idle'
|
|
102 elif (self.m.currState == 'heat'):
|
|
103 # Ditto
|
|
104 if (diff - self.m.minHeatOvershoot > 0 and self.m.minHeatOnTime + self.m.lastHeatOn < time.time()):
|
|
105 nextState = 'idle'
|
|
106 else:
|
|
107 # Not possible..
|
|
108 raise KeyError
|
|
109
|
|
110 log.debug("%3.2f %3.2f %3.2f %s %s" %
|
|
111 (self.m.temps[self.m.fermenterId],
|
|
112 self.m.temps[self.m.fridgeId], self.m.temps[self.m.ambientId],
|
|
113 self.m.currState, nextState))
|
|
114
|
|
115 if (nextState != "-"):
|
|
116 self.m.setState(nextState)
|
|
117
|
|
118 self.cv.wait(self.pollInterval)
|
|
119
|
|
120
|