changeset 861:10b1e6a1b408

thermostat program and raspberry pi buttons reader Ignore-this: aed325a19554a509a19747c89079597b darcs-hash:20130210102208-312f9-40fd7de854665d9f78e4788ddb4ea15062d52614
author drewp <drewp@bigasterisk.com>
date Sun, 10 Feb 2013 02:22:08 -0800
parents db20fbc4ca10
children 8e81ea96f41e
files service/thermostat/rpi_buttons.py service/thermostat/thermostat.py
diffstat 2 files changed, 214 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/thermostat/rpi_buttons.py	Sun Feb 10 02:22:08 2013 -0800
@@ -0,0 +1,64 @@
+#!/usr/bin/python3
+"""
+read button and knob on rpi, send back to thermostat program
+"""
+from RPi.GPIO import setmode, PUD_UP, setup, BCM, IN, input as pinInput
+import time, json
+# using a copy of urllib/request.py from py3.3 which supports 'method'
+from request import urlopen, Request
+
+requestedTemperature = 'http://bang.bigasterisk.com:10001/requestedTemperature'
+requestOpts = dict(headers={'user-agent': 'rpi buttons'})
+
+def getRequestedTemp():
+    return (json.loads(urlopen(
+        Request(requestedTemperature, **requestOpts)).read().decode('utf-8'))
+    )['tempF']
+
+def setRequestedTemp(f):
+    urlopen(Request(url=requestedTemperature,
+            method='PUT',
+            data=json.dumps({'tempF' : f}).encode('utf-8'),
+            **requestOpts))
+
+PIN_KNOB_A = 1
+PIN_KNOB_B = 4
+PIN_BUTTON = 0
+
+setmode(BCM)
+setup(PIN_KNOB_A, IN, PUD_UP)
+setup(PIN_KNOB_B, IN, PUD_UP)
+setup(PIN_BUTTON, IN, PUD_UP)
+
+print("reading knob and button, writing to %s" % requestedTemperature)
+prev = None
+prevButton = 0
+buttonHold = 0
+step = .05
+while True:
+    a, b = pinInput(PIN_KNOB_A), pinInput(PIN_KNOB_B)
+    button = not pinInput(PIN_BUTTON)
+    pos = (b * 2) + (a ^ b)
+
+    if prev is None:
+        prev = pos
+    dpos = (pos - prev) % 4
+
+    if dpos == 1:
+        print ("up")
+        setRequestedTemp(getRequestedTemp() + 1)
+    elif dpos == -1 % 4:
+        print ("down")
+        setRequestedTemp(getRequestedTemp() - 1)
+    else:
+        pass # 0 or unknown
+    prev = pos
+    
+    if button:
+        buttonHold += 1
+        if buttonHold == int(.1 / step):
+            print ("button to", button)
+    else:
+        buttonHold = 0
+
+    time.sleep(step)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/thermostat/thermostat.py	Sun Feb 10 02:22:08 2013 -0800
@@ -0,0 +1,150 @@
+from __future__ import division
+"""
+drives the heater output pin according to a requested temperature that you can edit. The temp is stored in mongodb.
+"""
+import cyclone.web, sys, urllib, time, pymongo, json, datetime
+from dateutil.tz import tzlocal
+from cyclone.httpclient import fetch
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.task import LoopingCall
+sys.path.append("/my/proj/homeauto/lib")
+from cycloneerr import PrettyErrorHandler
+from logsetup import log
+
+db = pymongo.Connection("bang")['thermostat']
+
+@inlineCallbacks
+def http(method, url, body=None):
+    resp = (yield fetch(url, method=method, postdata=body,
+                        headers={'user-agent': ['thermostat']}))
+    if resp.code != 200:
+        raise ValueError("%s returned %s: %s" % (url, resp.code, resp.body))
+    returnValue(resp.body)
+
+class Unknown(object):
+    pass
+    
+class Therm(object):
+    def __init__(self):
+        self._lastOn = Unknown
+        self._lastOff = time.time() - 1000
+
+        # the non-logging path
+        self.graphite = 'http://bang:9037/render' 
+        
+        # get this from devices.n3
+        self.heaterPin = 'http://bang:9056/pin/d4'
+
+    def getRequest(self):
+        return (db['request'].find_one(sort=[('t', -1)]) or
+                {'tempF':60}
+               )['tempF']
+    
+    def setRequest(self, f):
+        db['request'].insert({'tempF': f, 't':datetime.datetime.now(tzlocal())})
+        self.step()
+        
+    @inlineCallbacks
+    def step(self):
+        roomF = yield self.getRoomTempF()
+        requestedF = self.getRequest()
+        active = yield self.active()
+        minsOff = self.minutesSinceOff()
+        minsOn = self.minutesSinceOn()
+
+        log.info("roomF=%(roomF)s requestedF=%(requestedF)s active=%(active)s "
+                 "minsOn=%(minsOn)s minsOff=%(minsOff)s" % vars())
+        if not active:
+            if roomF < requestedF - 1:
+                if minsOff > 5:
+                    log.info("start heater")
+                    self.startCycle()
+                else:
+                    log.info("wait to start")
+            else:
+                pass
+        else:
+            if roomF > requestedF + 1:
+                log.info("stop heater")
+                self.stopCycle()
+            elif minsOn > 50:
+                log.info("heater on too long- stopping")
+                self.stopCycle()
+            else:
+                log.info("ok to keep warming")
+
+    @inlineCallbacks
+    def getRoomTempF(self):
+        target = 'system.house.temp.livingRoom'
+        body = (yield http('GET', self.graphite + '?' +
+                           urllib.urlencode({
+                               'target':"keepLastValue(%s)" % target,
+                               'rawData':'true',
+                               'from':'-60minutes',
+                           })))
+        latest = float(body.split(',')[-1])
+        returnValue(latest)
+
+    @inlineCallbacks
+    def active(self):
+        ret = yield http('GET', self.heaterPin)
+        returnValue(bool(int(ret.strip())))
+
+    @inlineCallbacks
+    def stopCycle(self):
+        log.info("heater off")
+        # need to make it be an output!
+        yield http('PUT', self.heaterPin, body='0')
+        self._lastOff = time.time()
+
+    @inlineCallbacks
+    def startCycle(self):
+        log.info("heater on")
+        yield http('PUT', self.heaterPin, body='1')
+        self._lastOn = time.time()
+        
+    def minutesSinceOff(self):
+        if self._lastOff is Unknown:
+            self._lastOff = time.time()
+            return 0
+        return (time.time() - self._lastOff) / 60
+
+    def minutesSinceOn(self):
+        if self._lastOn is Unknown:
+            self._lastOn = time.time()
+            return 0
+        return (time.time() - self._lastOn) / 60
+        
+
+class Index(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        self.write("thermostat. requested temp is %s." %
+                   self.settings.therm.getRequest())
+
+class RequestedTemperature(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        self.write(json.dumps({"tempF" : self.settings.therm.getRequest()}))
+    def put(self):
+        f = json.loads(self.request.body)['tempF']
+        if not isinstance(f, (int, float)):
+            raise TypeError("tempF was %r" % f)
+        self.settings.therm.setRequest(f)
+        self.write("ok")
+        
+if __name__ == '__main__':
+    t = Therm()
+    def step():
+        try:
+            t.step()
+        except Exception, e:
+            log.warn(e)
+            
+    LoopingCall(step).start(interval=30)
+    
+    reactor.listenTCP(10001, cyclone.web.Application([
+        (r'/', Index),
+        (r'/requestedTemperature', RequestedTemperature),
+        ], therm=t))
+    
+    reactor.run()