Mercurial > ~darius > hgwebdir.cgi > beermon
comparison beermon.py @ 0:1c6f5a0281c7
Initial revision
author | darius |
---|---|
date | Sun, 23 Sep 2007 03:17:47 +0000 |
parents | |
children | c0b01c8c63eb |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:1c6f5a0281c7 |
---|---|
1 #!/usr/bin/env python | |
2 | |
3 import pexpect, re, threading, time, logging | |
4 from logging.handlers import RotatingFileHandler | |
5 | |
6 class ROMReadError(Exception): | |
7 pass | |
8 | |
9 class Control(): | |
10 targetTemp = 18 | |
11 hysteresis = 4 | |
12 pollInterval = 30 | |
13 | |
14 def __init__(self, m): | |
15 self.m = m | |
16 self.initLog() | |
17 | |
18 def initLog(self): | |
19 # Init our logging | |
20 global log | |
21 log = logging.getLogger("monitor") | |
22 | |
23 # Default to warts and all logging | |
24 log.setLevel(logging.DEBUG) | |
25 | |
26 # Log to this file | |
27 logfile = logging.handlers.RotatingFileHandler(filename = "/tmp/monitor.log", | |
28 maxBytes = 10000, backupCount = 3) | |
29 | |
30 # And stderr | |
31 logstderr = logging.StreamHandler() | |
32 | |
33 # Format it nicely | |
34 formatter = logging.Formatter(fmt = "%(asctime)s: %(message)s", datefmt = "%Y%m%d %H:%M:%S") | |
35 | |
36 # Glue it all together | |
37 logfile.setFormatter(formatter) | |
38 logstderr.setFormatter(formatter) | |
39 log.addHandler(logfile) | |
40 log.addHandler(logstderr) | |
41 return(log) | |
42 | |
43 def doit(self): | |
44 log.debug("=== Inited ===") | |
45 log.debug("target temperature - %3.2f" % (self.targetTemp)) | |
46 log.debug("fermenterId - %s" % (self.m.fermenterId)) | |
47 log.debug("fridgeId - %s" % (self.m.fridgeId)) | |
48 log.debug("ambientId - %s" % (self.m.ambientId)) | |
49 log.debug("minCoolOnTime - %d, minCoolOffTime - %d" % (self.m.minCoolOnTime, self.m.minCoolOffTime)) | |
50 log.debug("minHeatOnTime - %d, minHeatOffTime - %d" % (self.m.minHeatOnTime, self.m.minHeatOffTime)) | |
51 log.debug("pollInterval - %d" % (self.pollInterval)) | |
52 | |
53 log.debug("=== Starting ===") | |
54 log.debug("Fermenter Fridge Ambient State New State") | |
55 while True: | |
56 if (self.m.lastUpdate == 0): | |
57 print "%s Invalid data" % (time.asctime()) | |
58 time.sleep(30) | |
59 self.m.setState('idle') | |
60 continue | |
61 | |
62 nextState = "-" | |
63 | |
64 diff = self.m.temps[self.m.fermenterId] - self.targetTemp | |
65 if (self.m.currState == 'idle'): | |
66 # If we're idle then only heat or cool if the temperate difference is out of the | |
67 # hysteresis range | |
68 if (abs(diff) > self.hysteresis): | |
69 if (diff < 0 and self.m.minHeatOffTime + self.m.lastHeatOff < time.time()): | |
70 nextState = 'heat' | |
71 elif (diff > 0 and self.m.minHeatOffTime + self.m.lastHeatOff < time.time()): | |
72 nextState = 'cool' | |
73 elif (self.m.currState == 'cool'): | |
74 # Go idle as soon as we can, there will be overshoot anyway | |
75 if (diff < 0 and self.m.minCoolOnTime + self.m.lastCoolOn < time.time()): | |
76 nextState = 'idle' | |
77 elif (self.m.currState == 'heat'): | |
78 if (diff > 0 and self.m.minHeatOnTime + self.m.lastHeatOn < time.time()): | |
79 nextState = 'idle' | |
80 else: | |
81 raise KeyError | |
82 | |
83 log.debug("%3.2f %3.2f %3.2f %s %s" % | |
84 (self.m.temps[self.m.fermenterId], | |
85 self.m.temps[self.m.fridgeId], self.m.temps[self.m.ambientId], | |
86 self.m.currState, nextState)) | |
87 if (nextState != "-"): | |
88 self.m.setState(nextState) | |
89 | |
90 time.sleep(self.pollInterval) | |
91 | |
92 | |
93 class MonitorDev(threading.Thread): | |
94 # Match a ROM ID (eg 00:11:22:33:44:55:66:77) | |
95 romre = re.compile('([0-9a-f]{2}:){7}[0-9a-f]{2}') | |
96 # Match the prompt | |
97 promptre = re.compile('> ') | |
98 | |
99 coolRelay = 7 | |
100 heatRelay = 6 | |
101 | |
102 fermenterId = '10:eb:48:21:01:08:00:df' | |
103 fridgeId = '10:a6:2a:c4:00:08:00:11' | |
104 ambientId = '10:97:1b:fe:00:08:00:d1' | |
105 | |
106 # minimum time the cooler must spend on/off | |
107 minCoolOnTime = 10 * 60 | |
108 minCoolOffTime = 10 * 60 | |
109 | |
110 # minimum time the heater must spend on/off | |
111 minHeatOnTime = 60 | |
112 minHeatOffTime = 60 | |
113 | |
114 temps = {} | |
115 lastUpdate = 0 | |
116 | |
117 # Lock to gate access to the comms | |
118 commsLock = None | |
119 | |
120 currState = 'idle' | |
121 | |
122 lastHeatOn = 0 | |
123 lastHeatOff = 0 | |
124 lastCoolOn = 0 | |
125 lastCoolOff = 0 | |
126 | |
127 def __init__(self): | |
128 threading.Thread.__init__(self) | |
129 self.commsLock = threading.Lock() | |
130 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)']) | |
131 assert(self.p.expect('logged in') == 0) | |
132 self.p.timeout = 3 | |
133 self.setspeed() | |
134 self.devs = self.find1wire() | |
135 self.temps = filter(self.istemp, self.devs) | |
136 | |
137 self.start() | |
138 | |
139 def setspeed(self): | |
140 self.commsLock.acquire() | |
141 self.p.send('~') | |
142 assert(self.p.expect('t - set terminal') == 0) | |
143 self.p.send('t') | |
144 assert(self.p.expect('p - set speed') == 0) | |
145 self.p.send('p') | |
146 assert(self.p.expect('f - 38400') == 0) | |
147 self.p.send('f') | |
148 assert(self.p.expect('done!') == 0) | |
149 self.commsLock.release() | |
150 | |
151 def find1wire(self): | |
152 self.commsLock.acquire() | |
153 self.p.sendline('') | |
154 assert(self.p.expect('> ') == 0) | |
155 self.p.sendline('sr') | |
156 # Echo | |
157 assert(self.p.expect('sr') == 0) | |
158 | |
159 # Send a new line which will give us a command prompt to stop on | |
160 # later. We could use read() but that would make the code a lot | |
161 # uglier | |
162 self.p.sendline('') | |
163 | |
164 devlist = [] | |
165 | |
166 # Loop until we get the command prompt (> ) collecting ROM IDs | |
167 while True: | |
168 idx = self.p.expect([self.romre, self.promptre]) | |
169 if (idx == 0): | |
170 # Matched a ROM | |
171 #print "Found ROM " + self.p.match.group() | |
172 devlist.append(self.p.match.group(0)) | |
173 elif (idx == 1): | |
174 # Matched prompt, exit | |
175 break | |
176 else: | |
177 # Unpossible! | |
178 self.commsLock.release() | |
179 raise SystemError() | |
180 | |
181 self.commsLock.release() | |
182 | |
183 return(devlist) | |
184 | |
185 def istemp(self, id): | |
186 [family, a, b, c, d, e, f, g] = id.split(':') | |
187 if (family == '10'): | |
188 return True | |
189 else: | |
190 return False | |
191 | |
192 def updateTemps(self): | |
193 tmp = {} | |
194 for i in self.temps: | |
195 tmp[i] = float(self.readTemp(i)) | |
196 | |
197 self.temps = tmp | |
198 self.lastUpdate = time.time() | |
199 return(self.temps) | |
200 | |
201 def readTemp(self, id): | |
202 self.commsLock.acquire() | |
203 cmd = 'te ' + id | |
204 self.p.sendline(cmd) | |
205 # Echo | |
206 assert(self.p.expect(cmd) == 0) | |
207 # Eat EOL left from expect | |
208 self.p.readline() | |
209 | |
210 line = self.p.readline().strip() | |
211 self.commsLock.release() | |
212 # 'CRC mismatch' can mean that we picked the wrong ROM.. | |
213 if (re.match('CRC mismatch', line) != None): | |
214 raise ROMReadError | |
215 | |
216 return(line) | |
217 | |
218 def setState(self, state): | |
219 if (state == 'cool'): | |
220 relay = 1 << self.coolRelay | |
221 elif (state == 'heat'): | |
222 relay = 1 << self.heatRelay | |
223 elif (state == 'idle'): | |
224 relay = 0 | |
225 else: | |
226 raise(ValueError) | |
227 | |
228 if (state == self.currState): | |
229 return | |
230 | |
231 # Keep track of when we last turned off or on | |
232 if (state == 'cool'): | |
233 if (self.currState == 'heat'): | |
234 self.lastHeatOff = time.time() | |
235 self.lastCoolOn = time.time() | |
236 elif (state == 'heat'): | |
237 if (self.currState == 'cool'): | |
238 self.lastCoolOff = time.time() | |
239 self.lastHeatOn = time.time() | |
240 else: | |
241 if (self.currState == 'cool'): | |
242 self.lastCoolOff = time.time() | |
243 if (self.currState == 'heat'): | |
244 self.lastHeatOff = time.time() | |
245 | |
246 self.currState = state | |
247 | |
248 self.commsLock.acquire() | |
249 # Need the extra spaces cause the parser in the micro is busted | |
250 cmd = 'out c %02x' % relay | |
251 self.p.sendline(cmd) | |
252 # Echo | |
253 assert(self.p.expect(cmd) == 0) | |
254 self.commsLock.release() | |
255 | |
256 def polltemps(self, temps): | |
257 while True: | |
258 for d in temps: | |
259 #print d | |
260 t = gettemp(p, d) | |
261 print "%s -> %s" % (d, t) | |
262 print | |
263 | |
264 def run(self): | |
265 while True: | |
266 self.updateTemps() | |
267 | |
268 def main(): | |
269 import time, monitor | |
270 | |
271 m = monitor.MonitorDev() | |
272 | |
273 try: | |
274 c = monitor.Control(m) | |
275 # Wait for the first temperature readings to come through, saves | |
276 # getting an 'invalid data' message | |
277 time.sleep(3) | |
278 c.doit() | |
279 log.debug("doit exited") | |
280 | |
281 except KeyboardInterrupt: | |
282 log.debug("Exiting due to keyboard interrupt") | |
283 | |
284 finally: | |
285 # Make sure we try and turn it off if something goes wrong | |
286 m.setState('idle') | |
287 | |
288 sys.exit(0) | |
289 | |
290 if __name__ == "__main__": | |
291 main() |