comparison beermon.py @ 6:45d9895a5020

Split into seperate files.
author darius
date Sat, 29 Sep 2007 14:39:59 +0000
parents 8d471840b153
children 17449d52d5e5
comparison
equal deleted inserted replaced
5:8d471840b153 6:45d9895a5020
2 2
3 ############################################################################ 3 ############################################################################
4 # Monitor & control fermenter temperature 4 # Monitor & control fermenter temperature
5 # v1.0 5 # v1.0
6 # 6 #
7 # $Id: beermon.py,v 1.6 2007/09/29 02:23:24 darius Exp $ 7 # $Id: beermon.py,v 1.7 2007/09/29 14:39:59 darius Exp $
8 # 8 #
9 # Depends on: Python 2.3 (I think) 9 # Depends on: Python 2.3 (I think)
10 # 10 #
11 ############################################################################ 11 ############################################################################
12 # 12 #
34 # SUCH DAMAGE. 34 # SUCH DAMAGE.
35 # 35 #
36 ############################################################################ 36 ############################################################################
37 37
38 38
39 import pexpect, re, threading, time, logging, sys, traceback 39 import time, logging, sys, traceback, ConfigParser, MonitorDev, Control
40 from logging.handlers import RotatingFileHandler 40 from logging.handlers import RotatingFileHandler
41
42 class ROMReadError(Exception):
43 pass
44
45 class ThreadDied(Exception):
46 pass
47
48 class Control():
49 targetTemp = 18
50 hysteresis = 0.5
51 pollInterval = 30
52 staleDataTime = 30
53
54 def __init__(self, m, _log):
55 self.m = m
56 global log
57 log = _log
58 self.cv = threading.Condition()
59 self.cv.acquire()
60
61 def doit(self):
62 log.debug("target temperature - %3.2f" % (self.targetTemp))
63 log.debug("fermenterId - %s" % (self.m.fermenterId))
64 log.debug("fridgeId - %s" % (self.m.fridgeId))
65 log.debug("ambientId - %s" % (self.m.ambientId))
66 log.debug("minCoolOnTime - %d, minCoolOffTime - %d" % (self.m.minCoolOnTime, self.m.minCoolOffTime))
67 log.debug("minHeatOnTime - %d, minHeatOffTime - %d" % (self.m.minHeatOnTime, self.m.minHeatOffTime))
68 log.debug("pollInterval - %d" % (self.pollInterval))
69
70 log.debug("=== Starting ===")
71 log.debug("Fermenter Fridge Ambient State New State")
72 while True:
73 # Check if our monitor thread has died
74 if (not self.m.isAlive()):
75 raise ThreadDied, "Monitor thread has died"
76
77 # Check for stale data
78 if (self.m.lastUpdate[self.m.fermenterId] + self.staleDataTime < time.time()):
79 log.debug("Stale data")
80 self.cv.wait(self.pollInterval)
81 self.m.setState('idle')
82 continue
83
84 # Work out what state we should go into
85 nextState = "-"
86 # Temperature diff, -ve => too cold, +ve => too warm
87 diff = self.m.temps[self.m.fermenterId] - self.targetTemp
88 if (self.m.currState == 'idle'):
89 # If we're idle then only heat or cool if the temperate difference is out of the
90 # hysteresis band
91 if (abs(diff) > self.hysteresis):
92 if (diff < 0 and self.m.minHeatOffTime + self.m.lastHeatOff < time.time()):
93 nextState = 'heat'
94 elif (diff > 0 and self.m.minHeatOffTime + self.m.lastHeatOff < time.time()):
95 nextState = 'cool'
96 elif (self.m.currState == 'cool'):
97 # Work out if we should go idle (based on min on time & overshoot)
98 if (diff + self.m.minCoolOvershoot < 0 and self.m.minCoolOnTime + self.m.lastCoolOn < time.time()):
99 nextState = 'idle'
100 elif (self.m.currState == 'heat'):
101 # Ditto
102 if (diff - self.m.minHeatOvershoot > 0 and self.m.minHeatOnTime + self.m.lastHeatOn < time.time()):
103 nextState = 'idle'
104 else:
105 # Not possible..
106 raise KeyError
107
108 log.debug("%3.2f %3.2f %3.2f %s %s" %
109 (self.m.temps[self.m.fermenterId],
110 self.m.temps[self.m.fridgeId], self.m.temps[self.m.ambientId],
111 self.m.currState, nextState))
112
113 if (nextState != "-"):
114 self.m.setState(nextState)
115
116 self.cv.wait(self.pollInterval)
117
118
119 class MonitorDev(threading.Thread):
120 # Match a ROM ID (eg 00:11:22:33:44:55:66:77)
121 romre = re.compile('([0-9a-f]{2}:){7}[0-9a-f]{2}')
122 # Match the prompt
123 promptre = re.compile('> ')
124
125 coolRelay = 7
126 heatRelay = 6
127
128 fermenterId = '10:eb:48:21:01:08:00:df'
129 fridgeId = '10:a6:2a:c4:00:08:00:11'
130 ambientId = '10:97:1b:fe:00:08:00:d1'
131
132 # minimum time the cooler must spend on/off
133 minCoolOnTime = 10 * 60
134 minCoolOffTime = 10 * 60
135
136 # minimum time the heater must spend on/off
137 minHeatOnTime = 60
138 minHeatOffTime = 60
139
140 # minimum to overshoot on heating/cooling
141 minHeatOvershoot = 1
142 minCoolOvershoot = 0
143
144 # Dictionary of sensor IDs & temperatures
145 temps = {}
146 # Dictionary of sensor IDs & epoch times
147 lastUpdate = {}
148
149 # List of all device IDs
150 devs = []
151 # List of temperature sensor IDs
152 tempdevs = []
153
154 # Lock to gate access to the comms
155 commsLock = None
156
157 currState = 'idle'
158
159 lastHeatOn = 0
160 lastHeatOff = 0
161 lastCoolOn = 0
162 lastCoolOff = 0
163
164 def __init__(self):
165 threading.Thread.__init__(self)
166 self.commsLock = threading.Lock()
167 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)'])
168 assert(self.p.expect('logged in') == 0)
169 self.p.timeout = 3
170 self.setspeed()
171 self.devs = self.find1wire()
172 self.tempdevs = filter(self.istemp, self.devs)
173
174 self.start()
175
176 def setspeed(self):
177 self.commsLock.acquire()
178 self.p.send('~')
179 assert(self.p.expect('t - set terminal') == 0)
180 self.p.send('t')
181 assert(self.p.expect('p - set speed') == 0)
182 self.p.send('p')
183 assert(self.p.expect('f - 38400') == 0)
184 self.p.send('f')
185 assert(self.p.expect('done!') == 0)
186 self.commsLock.release()
187
188 def find1wire(self):
189 self.commsLock.acquire()
190 self.p.sendline('')
191 assert(self.p.expect('> ') == 0)
192 self.p.sendline('sr')
193 # Echo
194 assert(self.p.expect('sr') == 0)
195
196 # Send a new line which will give us a command prompt to stop on
197 # later. We could use read() but that would make the code a lot
198 # uglier
199 self.p.sendline('')
200
201 devlist = []
202
203 # Loop until we get the command prompt (> ) collecting ROM IDs
204 while True:
205 idx = self.p.expect([self.romre, self.promptre])
206 if (idx == 0):
207 # Matched a ROM
208 #print "Found ROM " + self.p.match.group()
209 devlist.append(self.p.match.group(0))
210 elif (idx == 1):
211 # Matched prompt, exit
212 break
213 else:
214 # Unpossible!
215 self.commsLock.release()
216 raise SystemError()
217
218 self.commsLock.release()
219
220 return(devlist)
221
222 def istemp(self, id):
223 [family, a, b, c, d, e, f, g] = id.split(':')
224 if (family == '10'):
225 return True
226 else:
227 return False
228
229 def updateTemps(self):
230 for i in self.tempdevs:
231 try:
232 self.temps[i] = float(self.readTemp(i))
233 self.lastUpdate[i] = time.time()
234 except ROMReadError:
235 # Ignore this - just results in no update reflected by lastUpdate
236 pass
237
238 return(self.temps)
239
240 def readTemp(self, id):
241 self.commsLock.acquire()
242 cmd = 'te ' + id
243 self.p.sendline(cmd)
244 # Echo
245 assert(self.p.expect(cmd) == 0)
246 # Eat EOL left from expect
247 self.p.readline()
248
249 line = self.p.readline().strip()
250 self.commsLock.release()
251 # 'CRC mismatch' can mean that we picked the wrong ROM..
252 if (re.match('CRC mismatch', line) != None):
253 raise ROMReadError
254
255 return(line)
256
257 def setState(self, state):
258 if (state == 'cool'):
259 relay = 1 << self.coolRelay
260 elif (state == 'heat'):
261 relay = 1 << self.heatRelay
262 elif (state == 'idle'):
263 relay = 0
264 else:
265 raise(ValueError)
266
267 if (state == self.currState):
268 return
269
270 # Keep track of when we last turned off or on
271 if (state == 'cool'):
272 if (self.currState == 'heat'):
273 self.lastHeatOff = time.time()
274 self.lastCoolOn = time.time()
275 elif (state == 'heat'):
276 if (self.currState == 'cool'):
277 self.lastCoolOff = time.time()
278 self.lastHeatOn = time.time()
279 else:
280 if (self.currState == 'cool'):
281 self.lastCoolOff = time.time()
282 if (self.currState == 'heat'):
283 self.lastHeatOff = time.time()
284
285 self.currState = state
286
287 self.commsLock.acquire()
288 # Need the extra spaces cause the parser in the micro is busted
289 cmd = 'out c %02x' % relay
290 self.p.sendline(cmd)
291 # Echo
292 assert(self.p.expect(cmd) == 0)
293 self.commsLock.release()
294
295 def polltemps(self, temps):
296 while True:
297 for d in temps:
298 #print d
299 t = gettemp(p, d)
300 print "%s -> %s" % (d, t)
301 print
302
303 def run(self):
304 while True:
305 self.updateTemps()
306 41
307 def initLog(): 42 def initLog():
308 # Init our logging 43 # Init our logging
309 log = logging.getLogger("monitor") 44 log = logging.getLogger("monitor")
310 45
327 log.addHandler(logfile) 62 log.addHandler(logfile)
328 log.addHandler(logstderr) 63 log.addHandler(logstderr)
329 return(log) 64 return(log)
330 65
331 def main(): 66 def main():
332 import beermon
333
334 global log 67 global log
335 log = initLog() 68 log = initLog()
336 69
70 conf = ConfigParser.ConfigParser()
71 conf.read('beermon.ini')
72
73 for s in ['control', 'hardware']:
74 if (not conf.has_section(s)):
75 log.debug("Mandatory '%s' section missing from config file, exiting" % (s))
76 sys.exit(1)
77
337 log.debug("=== Initing ===") 78 log.debug("=== Initing ===")
338 log.debug("$Id: beermon.py,v 1.6 2007/09/29 02:23:24 darius Exp $") 79 log.debug("$Id: beermon.py,v 1.7 2007/09/29 14:39:59 darius Exp $")
339 80
340 m = None 81 try:
82 m = MonitorDev.MonitorDev(log, conf)
83 c = Control.Control(log, m, conf)
84 except ConfigParser.NoOptionError, e:
85 log.debug("Mandatory option '%s' missing from section '%s'" % (e.option, e.section))
86 sys.exit(1)
87 except ValueError, e:
88 log.debug("Unable to parse option - " + str(e))
89
341 exitCode = 0 90 exitCode = 0
342 try: 91 try:
343 m = beermon.MonitorDev()
344
345 c = beermon.Control(m, log)
346 # Wait for the first temperature readings to come through, saves 92 # Wait for the first temperature readings to come through, saves
347 # getting an 'invalid data' message 93 # getting an 'invalid data' message
94 # XXX: sleep on condvar holding data?
348 time.sleep(3) 95 time.sleep(3)
349 c.doit() 96 c.doit()
350 log.debug("doit exited") 97 log.debug("doit exited")
351 98
352 except KeyboardInterrupt: 99 except KeyboardInterrupt:
356 log.debug("Something went wrong, details below:") 103 log.debug("Something went wrong, details below:")
357 log.debug(e) 104 log.debug(e)
358 log.debug(reduce(lambda x, y: x + y, traceback.format_exception( 105 log.debug(reduce(lambda x, y: x + y, traceback.format_exception(
359 sys.exc_type, sys.exc_value, sys.exc_traceback))) 106 sys.exc_type, sys.exc_value, sys.exc_traceback)))
360 exitCode = 1 107 exitCode = 1
361 108
362 finally: 109 finally:
363 # Make sure we try and turn it off if something goes wrong 110 # Make sure we try and turn it off if something goes wrong
364 if (m != None): 111 if (m != None):
365 m.setState('idle') 112 m.setState('idle')
366 113