changeset 293:d15dda1b1f76

merge
author Matt Johnston <matt@ucc.asn.au>
date Sat, 06 Jul 2019 18:29:45 +0800
parents 28eb733cb803 (current diff) f7261dd970da (diff)
children 6bacd8ca9f8f
files py/uploader.py
diffstat 30 files changed, 1989 insertions(+), 989 deletions(-) [+]
line wrap: on
line diff
--- a/py/config.py	Thu Mar 19 21:50:52 2015 +0800
+++ b/py/config.py	Sat Jul 06 18:29:45 2019 +0800
@@ -13,15 +13,16 @@
 PARAMS_FILE = os.path.join(os.path.dirname(__file__), 'tempserver.conf')
 
 SENSOR_BASE_DIR = '/sys/devices/w1_bus_master1'
-FRIDGE_GPIO = '/sys/devices/virtual/gpio/gpio17'
+FRIDGE_GPIO_PIN = 17
 WORT_NAME = '28-0000042cf4dd'
 FRIDGE_NAME = '28-0000042cccc4'
 AMBIENT_NAME = '28-0000042c6dbb'
 INTERNAL_TEMPERATURE = '/sys/class/thermal/thermal_zone0/temp'
 
 HMAC_KEY = "a key"
-#UPDATE_URL = 'https://matt.ucc.asn.au/test/templog/update'
-UPDATE_URL = 'https://evil.ucc.asn.au/~matt/templog/update'
+SERVER_URL = 'https://evil.ucc.asn.au/~matt/templog'
+UPDATE_URL = "%s/update" % SERVER_URL
+SETTINGS_URL = "%s/get_settings" % SERVER_URL
 
 # site-local values overridden in localconfig, eg WORT_NAME, HMAC_KEY
 try:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/py/configwaiter.py	Sat Jul 06 18:29:45 2019 +0800
@@ -0,0 +1,62 @@
+import asyncio
+import aiohttp
+
+import utils
+from utils import L,D,EX,W,E
+import config
+
+class ConfigWaiter(object):
+    """ Waits for config updates from the server. http long polling """
+
+    def __init__(self, server):
+        self.server = server
+        self.epoch_tag = None
+        self.http_session = aiohttp.ClientSession()
+
+    @asyncio.coroutine
+    def run(self):
+        # wait until someting has been uploaded (the uploader itself waits 5 seconds)
+        yield from asyncio.sleep(10)
+        while True:
+            yield from self.do()
+
+            # avoid spinning too fast
+            yield from asyncio.sleep(1)
+
+    @asyncio.coroutine
+    def do(self):
+        try:
+            if self.epoch_tag:
+                headers = {'etag': self.epoch_tag}
+            else:
+                headers = None
+
+            r = yield from asyncio.wait_for(
+                self.http_session.get(config.SETTINGS_URL, headers=headers), 
+                300)
+            D("waiter status %d" % r.status)
+            if r.status == 200:
+                rawresp = yield from asyncio.wait_for(r.text(), 600)
+
+                resp = utils.json_load_round_float(rawresp)
+
+                self.epoch_tag = resp['epoch_tag']
+                D("waiter got epoch tag %s" % self.epoch_tag)
+                epoch = self.epoch_tag.split('-')[0]
+                if self.server.params.receive(resp['params'], epoch):
+                    self.server.reload_signal(True)
+            elif r.status == 304:
+                pass
+            else:
+                # longer timeout to avoid spinning
+                yield from asyncio.sleep(30)
+
+        except asyncio.TimeoutError:
+            D("configwaiter http timed out")
+            pass
+        except Exception as e:
+            EX("Error watching config: %s" % str(e))
+
+
+
+
--- a/py/fridge.py	Thu Mar 19 21:50:52 2015 +0800
+++ b/py/fridge.py	Sat Jul 06 18:29:45 2019 +0800
@@ -1,60 +1,46 @@
 # -*- coding: utf-8 -*-
+import asyncio
+
 from utils import L,W,E,EX,D
 import config
-import gevent
 
-class Fridge(gevent.Greenlet):
+import gpio
+
+class Fridge(object):
 
     OVERSHOOT_MAX_DIV = 1800.0 # 30 mins
 
     def __init__(self, server):
-        gevent.Greenlet.__init__(self)
         self.server = server
-        self.setup_gpio()
+        self.gpio = gpio.Gpio(config.FRIDGE_GPIO_PIN, "fridge")
         self.wort_valid_clock = 0
         self.fridge_on_clock = 0
         self.off()
 
-    def setup_gpio(self):
-        dir_fn = '%s/direction' % config.FRIDGE_GPIO
-        with open(dir_fn, 'w') as f:
-            f.write('low')
-        val_fn = '%s/value' % config.FRIDGE_GPIO
-        # XXX - Fridge should have __enter__/__exit__, close the file there.
-        self.value_file = open(val_fn, 'r+')
-
     def turn(self, value):
-        self.value_file.seek(0)
-        if value:
-            self.value_file.write('1')
-        else:
-            self.value_file.write('0')
-        self.value_file.flush()
+        self.gpio.turn(value)
 
     def on(self):
         self.turn(True)
+        pass
 
     def off(self):
         self.turn(False)
         self.fridge_off_clock = self.server.now()
 
     def is_on(self):
-        self.value_file.seek(0)
-        buf = self.value_file.read().strip()
-        if buf == '0':
-            return False
-        if buf != '1':
-            E("Bad value read from gpio '%s': '%s'" 
-                % (self.value_file.name, buf))
-        return True
+        return self.gpio.get_state()
 
-    # greenlet subclassed
-    def _run(self):
+    @asyncio.coroutine
+    def run(self):
         if self.server.params.disabled:
             L("Fridge is disabled")
         while True:
-            self.do()
-            self.server.sleep(config.FRIDGE_SLEEP)
+            try:
+                self.do()
+                yield from self.server.sleep(config.FRIDGE_SLEEP)
+            except Exception as e:
+                EX("fridge failed")
 
     def do(self):
         """ this is the main fridge control logic """
@@ -96,6 +82,8 @@
         if fridge is None:
             W("Invalid fridge sensor")
 
+        D("fridge on %s" % self.is_on())
+
         if self.is_on():
             turn_off = False
             on_time = self.server.now() - self.fridge_on_clock
@@ -124,6 +112,7 @@
         else:
             # fridge is off
             turn_on = False
+            D("fridge %(fridge)s max %(fridge_max)s wort %(wort)s wort_max %(wort_max)s" % locals())
             if not params.nowort \
                 and wort is not None \
                 and wort >= wort_max:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/py/gpio.py	Sat Jul 06 18:29:45 2019 +0800
@@ -0,0 +1,4 @@
+try:
+	from gpio_rpi import *
+except ImportError:
+	from gpio_test import *
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/py/gpio_rpi.py	Sat Jul 06 18:29:45 2019 +0800
@@ -0,0 +1,49 @@
+import os
+
+import RPi.GPIO as GPIO
+
+from utils import L,D,EX,W
+
+__all__ = ["Gpio"]
+
+class Gpio(object):
+    SYS_GPIO_BASE = '/sys/class/gpio/gpio'
+    def __init__(self, pin, name):
+        self.pin = pin
+        self.name = name
+
+        dir_fn = '%s%d/direction' % (self.SYS_GPIO_BASE, pin)
+        with open(dir_fn, 'w') as f:
+            # make sure it doesn't start "on"
+            f.write('low')
+        val_fn = '%s%d/value' % (self.SYS_GPIO_BASE, pin)
+        self.value_file = open(val_fn, 'r+')
+
+    def turn(self, value):
+        self.value_file.seek(0)
+        self.value_file.write('1' if value else '0')
+        self.value_file.flush()
+
+    def get_state(self):
+        self.value_file.seek(0)
+        buf = self.value_file.read().strip()
+        if buf == '0':
+            return False
+        if buf != '1':
+            E("Bad value read from gpio '%s': '%s'" 
+                % (self.value_file.name, buf))
+        return True
+
+
+def main():
+    g = Gpio(17, 'f')
+    g.turn(1)
+
+    print(g.get_state())
+
+    g.turn(0)
+
+    print(g.get_state())
+
+if __name__ == '__main__':
+    main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/py/gpio_test.py	Sat Jul 06 18:29:45 2019 +0800
@@ -0,0 +1,23 @@
+import os
+
+from utils import L,D,EX,W
+
+__all__ = ["Gpio"]
+
+class Gpio(object):
+	def __init__(self, pin, name):
+		self.name = name
+		self.pin = name
+		self.state = False
+		L("Test GPIO %s pin %d started, set off." % (name, pin))
+
+	def turn(self, value):
+		self.state = bool(value)
+		onoff = ("off", "on")[int(self.state)]
+		L("Test GPIO %s pin %s turned %s" % (self.name, self.pin, onoff))
+
+	def get_state(self):
+		return self.state
+		
+
+	
--- a/py/params.py	Thu Mar 19 21:50:52 2015 +0800
+++ b/py/params.py	Sat Jul 06 18:29:45 2019 +0800
@@ -2,12 +2,13 @@
 import collections
 import json
 import signal
-import StringIO
-
-import gevent
+import tempfile
+import os
+import binascii
 
 import config
 from utils import W,L,E,EX
+import utils
 
 _FIELD_DEFAULTS = {
     'fridge_setpoint': 16,
@@ -26,6 +27,7 @@
 
     def __init__(self):
         self.update(_FIELD_DEFAULTS)
+        self._set_epoch(None)
 
     def __getattr__(self, k):
         return self[k]
@@ -35,16 +37,14 @@
         self[k]
         self[k] = v
 
-    def load(self, f = None):
-        if not f:
-            try:
-                f = file(config.PARAMS_FILE, 'r')
-            except IOError, e:
-                W("Missing parameter file, using defaults. %s", e)
-                return
+    def _set_epoch(self, epoch):
+        # since __setattr__ is overridden
+        object.__setattr__(self, '_epoch', epoch)
+
+    def _do_load(self, f):
         try:
-            u = json.load(f)
-        except Exception, e:
+            u = utils.json_load_round_float(f.read())
+        except Exception as e:
             raise self.Error(e)
 
         for k in u:
@@ -53,19 +53,77 @@
             if k not in self:
                 raise self.Error("Unknown parameter %s=%s in file '%s'" % (str(k), str(u[k]), getattr(f, 'name', '???')))
         self.update(u)
+        # new epoch, 120 random bits
+        self._set_epoch(binascii.hexlify(os.urandom(15)).decode())
 
         L("Loaded parameters")
         L(self.save_string())
 
+    def load(self, f = None):
+        if f:
+            return self._do_load(f)
+        else:
+            with open(config.PARAMS_FILE, 'r') as f:
+                try:
+                    return self._do_load(f)
+                except IOError as e:
+                    W("Missing parameter file, using defaults. %s" % str(e))
+                    return
 
-    def save(self, f = None):
-        if not f:
-            f = file(config.PARAMS_FILE, 'w')
-        json.dump(self, f, sort_keys=True, indent=4)
-        f.write('\n')
-        f.flush()
+    def get_epoch(self):
+        return self._epoch
+
+    def receive(self, params, epoch):
+        """ updates parameters from the server. does some validation,
+        writes config file to disk.
+        Returns True on success, False failure 
+        """
+
+        if epoch != self._epoch:
+            return
+
+        def same_type(a, b):
+            ta = type(a)
+            tb = type(b)
+
+            if ta == int:
+                ta = float
+            if tb == int:
+                tb = float
+
+            return ta == tb
+
+        if self.keys() != params.keys():
+            diff = self.keys() ^ params.keys()
+            E("Mismatching params, %s" % str(diff))
+            return False
+
+        for k, v in params.items():
+            if not same_type(v, self[k]):
+                E("Bad type for %s" % k)
+                return False
+
+        dir = os.path.dirname(config.PARAMS_FILE)
+        try:
+            t = tempfile.NamedTemporaryFile(prefix='config',
+                mode='w+t', # NamedTemporaryFile is binary by default
+                dir = dir,
+                delete = False)
+
+            out = json.dumps(params, sort_keys=True, indent=4)+'\n'
+            t.write(out)
+            name = t.name
+            t.close()
+
+            os.rename(name, config.PARAMS_FILE)
+        except Exception as e:
+            EX("Problem: %s" % e)
+            return False
+
+        self.update(params)
+        L("Received parameters")
+        L(self.save_string())
+        return True
 
     def save_string(self):
-        s = StringIO.StringIO()
-        self.save(s)
-        return s.getvalue()
+        return json.dumps(self, sort_keys=True, indent=4)
--- a/py/receive.py	Thu Mar 19 21:50:52 2015 +0800
+++ b/py/receive.py	Sat Jul 06 18:29:45 2019 +0800
@@ -28,8 +28,8 @@
 
     def_params = params.Params()
 
-    if def_params.viewkeys() != new_params.viewkeys():
-        diff = def_params.viewkeys() ^ new_params.viewkeys()
+    if def_params.keys() != new_params.keys():
+        diff = def_params.keys() ^ new_params.keys()
         return "Mismatching params, %s" % str(diff)
 
     for k, v in new_params.items():
@@ -48,7 +48,7 @@
         t.close()
 
         os.rename(name, config.PARAMS_FILE)
-    except Exception, e:
+    except Exception as e:
         return "Problem: %s" % e
 
     try:
@@ -56,11 +56,11 @@
         if pid < 2:
             return "Bad pid %d" % pid
         os.kill(pid, signal.SIGHUP)
-    except Exception, e:
+    except Exception as e:
         return "HUP problem: %s" % e
 
     return 'Good Update'
 
 if __name__ == '__main__':
-    print main()
+    print(main())
 
--- a/py/requirements.txt	Thu Mar 19 21:50:52 2015 +0800
+++ b/py/requirements.txt	Sat Jul 06 18:29:45 2019 +0800
@@ -1,17 +1,9 @@
-argparse==1.2.1
-wsgiref==0.1.2
-
-# sha256: v6nYRtuRp9i2o26HNT7tZBx-Pn0L-guZdXltIn8ttOs
-gevent==1.0
-
-# sha256: sWDlVqIuFrrj8_Y__OeJhoLIA82JZFcZL3tU_nT-mR4
-greenlet==0.4.2
+# sha256: nkIlLxfR3YnuMXReDE--WIYsJRR-sO9SlcnNm8tOosE
+lockfile==0.10.2
 
-# sha256: I9pYnJH1nLfGRNXOXfU51Eg0G9R5kX1t3pc_guJxkUc
-lockfile==0.9.1
+# sha256: 2zFqD89UuXAsr2ymGbdr4l1T9e4Hgbr_C7ni4DVfryQ
+python-daemon==2.0.5
 
-# sha256: FmX7Fr_q5y8Wqi3kC8dWYUWL1Ccxp9RjqRGo1er5bAs
-python-daemon==1.6
+# sha256: 6vR5rMmP_uCgKYgkZevyHzwwLhuUpBsWyKWmlbxhSQA
+aiohttp==0.16.3
 
-# sha256: NkiAJJLpVf_rKPbauGStcUBZ9UOL9nmNgvnUd8ZmrKM
-requests==2.3.0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/py/sensor.py	Sat Jul 06 18:29:45 2019 +0800
@@ -0,0 +1,10 @@
+import os
+
+def make_sensor(server):
+    if server.test_mode():
+        import sensor_test
+        return sensor_test.SensorTest(server)
+    else:
+        import sensor_ds18b20
+        return sensor_ds18b20.SensorDS18B20(server)
+
--- a/py/sensor_ds18b20.py	Thu Mar 19 21:50:52 2015 +0800
+++ b/py/sensor_ds18b20.py	Sat Jul 06 18:29:45 2019 +0800
@@ -2,27 +2,26 @@
 
 import os
 import re
-
-import gevent
-import gevent.threadpool
+import asyncio
+import concurrent.futures
 
 import config
 from utils import D,L,W,E,EX
 
-class DS18B20s(gevent.Greenlet):
+class SensorDS18B20(object):
 
     THERM_RE = re.compile('.* YES\n.*t=(.*)\n', re.MULTILINE)
 
     def __init__(self, server):
-        gevent.Greenlet.__init__(self)
         self.server = server
-        self.readthread = gevent.threadpool.ThreadPool(1)
+        self.readthread = concurrent.futures.ThreadPoolExecutor(max_workers=1)
         self.master_dir = config.SENSOR_BASE_DIR
 
+    @asyncio.coroutine
     def do(self):
         vals = {}
         for n in self.sensor_names():
-                value = self.do_sensor(n)
+                value = yield from self.do_sensor(n)
                 if value is not None:
                     vals[n] = value
 
@@ -32,26 +31,31 @@
 
         self.server.add_reading(vals)
 
-    def _run(self):
+    @asyncio.coroutine
+    def run(self):
         while True:
-            self.do()
-            self.server.sleep(config.SENSOR_SLEEP)
+            yield from self.do()
+            yield from asyncio.sleep(config.SENSOR_SLEEP)
 
+
+    @asyncio.coroutine
     def read_wait(self, f):
-        # handles a blocking file read with a gevent threadpool. A
-        # real python thread performs the read while other gevent
-        # greenlets keep running.
+        # handles a blocking file read with a threadpool. A
+        # real python thread performs the read while other 
+        # asyncio tasks keep running.
         # the ds18b20 takes ~750ms to read, which is noticable
         # interactively.
-        return self.readthread.apply(f.read)
+        loop = asyncio.get_event_loop()
+        return loop.run_in_executor(None, f.read)
 
+    @asyncio.coroutine
     def do_sensor(self, s, contents = None):
         """ contents can be set by the caller for testing """
         try:
             if contents is None:
                 fn = os.path.join(self.master_dir, s, 'w1_slave')
-                f = open(fn, 'r')
-                contents = self.read_wait(f)
+                with open(fn, 'r') as f:
+                    contents = yield from self.read_wait(f)
 
             match = self.THERM_RE.match(contents)
             if match is None:
@@ -62,14 +66,15 @@
                 E("Problem reading sensor '%s': %f" % (s, temp))
                 return None
             return temp
-        except Exception, e:
+        except Exception as e:
             EX("Problem reading sensor '%s': %s" % (s, str(e)))
             return None
 
     def do_internal(self):
         try:
-            return int(open(config.INTERNAL_TEMPERATURE, 'r').read()) / 1000.0
-        except Exception, e:
+            with open(config.INTERNAL_TEMPERATURE, 'r') as f:
+                return int(f.read()) / 1000.0
+        except Exception as e:
             EX("Problem reading internal sensor: %s" % str(e))
             return None
         
@@ -77,7 +82,8 @@