Mercurial > templog
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 @@