# HG changeset patch # User Matt Johnston # Date 1562409025 -28800 # Node ID 6bacd8ca9f8fbeff93c0f513eacf4a60392b1621 # Parent d15dda1b1f760dfc23f979b5d71dcd11c82462fe# Parent 97d99eb42d27afd4e6093a085c6d19e241696f85 merge diff -r d15dda1b1f76 -r 6bacd8ca9f8f py/config.py --- a/py/config.py Sat Jul 06 18:29:45 2019 +0800 +++ b/py/config.py Sat Jul 06 18:30:25 2019 +0800 @@ -8,15 +8,18 @@ FRIDGE_WORT_INVALID_TIME = 300 # 5 mins # 12 hours of "offline" readings stored -MAX_READINGS = 12*60*60 / SENSOR_SLEEP +MAX_READINGS = 12*60*60 // SENSOR_SLEEP PARAMS_FILE = os.path.join(os.path.dirname(__file__), 'tempserver.conf') SENSOR_BASE_DIR = '/sys/devices/w1_bus_master1' FRIDGE_GPIO_PIN = 17 -WORT_NAME = '28-0000042cf4dd' -FRIDGE_NAME = '28-0000042cccc4' -AMBIENT_NAME = '28-0000042c6dbb' +#WORT_NAME = '28-0000042cf4dd' +#FRIDGE_NAME = '28-0000042cccc4' +#AMBIENT_NAME = '28-0000042c6dbb' +AMBIENT_NAME = 'missingambient' +FRIDGE_NAME = '28-0000042c6dbb' +WORT_NAME = '28-0000042cccc4' # was fridge INTERNAL_TEMPERATURE = '/sys/class/thermal/thermal_zone0/temp' HMAC_KEY = "a key" diff -r d15dda1b1f76 -r 6bacd8ca9f8f py/configwaiter.py --- a/py/configwaiter.py Sat Jul 06 18:29:45 2019 +0800 +++ b/py/configwaiter.py Sat Jul 06 18:30:25 2019 +0800 @@ -1,4 +1,6 @@ import asyncio +import datetime + import aiohttp import utils @@ -12,6 +14,7 @@ self.server = server self.epoch_tag = None self.http_session = aiohttp.ClientSession() + self.limitlog = utils.NotTooOften(datetime.timedelta(minutes=15)) @asyncio.coroutine def run(self): @@ -49,14 +52,13 @@ pass else: # longer timeout to avoid spinning + text = yield from asyncio.wait_for(r.text(), 600) + D("Bad server response. %d %s" % (r.status, text)) yield from asyncio.sleep(30) - except asyncio.TimeoutError: - D("configwaiter http timed out") - pass + except aiohttp.errors.ClientError as e: + self.limitlog.log("Error with configwaiter: %s" % str(e)) + except asyncio.TimeoutError as e: + self.limitlog.log("configwaiter http timed out: %s" % str(e)) except Exception as e: EX("Error watching config: %s" % str(e)) - - - - diff -r d15dda1b1f76 -r 6bacd8ca9f8f py/fridge.py --- a/py/fridge.py Sat Jul 06 18:29:45 2019 +0800 +++ b/py/fridge.py Sat Jul 06 18:30:25 2019 +0800 @@ -5,24 +5,28 @@ import config import gpio +import utils class Fridge(object): OVERSHOOT_MAX_DIV = 1800.0 # 30 mins - def __init__(self, server): + def __init__(self, server, nowait = False): self.server = server self.gpio = gpio.Gpio(config.FRIDGE_GPIO_PIN, "fridge") + self.integrator = utils.StepIntegrator(self.server.now, self.server.params.overshoot_delay) self.wort_valid_clock = 0 self.fridge_on_clock = 0 self.off() + if nowait: + self.fridge_off_clock = 0 def turn(self, value): self.gpio.turn(value) + self.integrator.turn(value) def on(self): self.turn(True) - pass def off(self): self.turn(False) @@ -59,6 +63,8 @@ if wort is not None: self.wort_valid_clock = self.server.now() + self.integrator.set_limit(params.overshoot_delay) + # Safety to avoid bad things happening to the fridge motor (?) # When it turns off don't start up again for at least FRIDGE_DELAY if not self.is_on() and off_time < config.FRIDGE_DELAY: @@ -86,18 +92,17 @@ if self.is_on(): turn_off = False - on_time = self.server.now() - self.fridge_on_clock + on_time = self.integrator.integrate() + on_percent = on_time / params.overshoot_delay - overshoot = 0 - if on_time > params.overshoot_delay: - overshoot = params.overshoot_factor \ - * min(self.OVERSHOOT_MAX_DIV, on_time) \ - / self.OVERSHOOT_MAX_DIV - D("on_time %(on_time)f, overshoot %(overshoot)f" % locals()) + overshoot = params.overshoot_factor * on_percent + D("on_time %(on_percent)f, overshoot %(overshoot)f" % locals()) if not params.nowort and wort is not None: if wort - overshoot < params.fridge_setpoint: - L("wort has cooled enough, %(wort)f" % locals() ) + max_div = self.OVERSHOOT_MAX_DIV + overshoot_factor = params.overshoot_factor + L("wort has cooled enough, %(wort)fº (overshoot %(overshoot)fº = %(overshoot_factor)f * min(%(on_time)f) / %(max_div)f)" % locals() ) turn_off = True elif fridge is not None and fridge < fridge_min: W("fridge off fallback, fridge %(fridge)f, min %(fridge_min)f" % locals()) diff -r d15dda1b1f76 -r 6bacd8ca9f8f py/sensor_ds18b20.py --- a/py/sensor_ds18b20.py Sat Jul 06 18:29:45 2019 +0800 +++ b/py/sensor_ds18b20.py Sat Jul 06 18:30:25 2019 +0800 @@ -62,7 +62,7 @@ D("no match") return None temp = int(match.groups(1)[0]) / 1000.0 - if temp > 80: + if temp > 80 or temp == 0: E("Problem reading sensor '%s': %f" % (s, temp)) return None return temp @@ -88,6 +88,8 @@ E("No W1 sensors found") return [] names = contents.split() + # only ds18b20 + names = [n for n in names if n.startswith('28-')] return names def wort_name(self): diff -r d15dda1b1f76 -r 6bacd8ca9f8f py/setup_gpio.sh --- a/py/setup_gpio.sh Sat Jul 06 18:29:45 2019 +0800 +++ b/py/setup_gpio.sh Sat Jul 06 18:30:25 2019 +0800 @@ -2,8 +2,12 @@ # this must run as root + PINS="17" for PIN in $PINS; do echo $PIN > /sys/class/gpio/export done + +chgrp gpio /sys/class/gpio/gpio17/direction /sys/class/gpio/gpio17/value +chmod g+w /sys/class/gpio/gpio17/direction /sys/class/gpio/gpio17/value diff -r d15dda1b1f76 -r 6bacd8ca9f8f py/tempserver.py --- a/py/tempserver.py Sat Jul 06 18:29:45 2019 +0800 +++ b/py/tempserver.py Sat Jul 06 18:30:25 2019 +0800 @@ -22,16 +22,17 @@ class Tempserver(object): - def __init__(self, test_mode): + def __init__(self, test_mode, nowait): self.readings = [] self.current = (None, None) self.fridge = None self._wakeup = asyncio.Event() self._test_mode = test_mode + self._nowait = nowait def __enter__(self): self.params = params.Params() - self.fridge = fridge.Fridge(self) + self.fridge = fridge.Fridge(self, self._nowait) self.uploader = uploader.Uploader(self) self.configwaiter = configwaiter.ConfigWaiter(self) self.params.load() @@ -130,12 +131,12 @@ if debug: level = logging.DEBUG logging.basicConfig(format='%(asctime)s %(message)s', - datefmt='%m/%d/%Y %I:%M:%S %p', + datefmt='%d/%m/%Y %I:%M:%S %p', level=level) #logging.getLogger("asyncio").setLevel(logging.DEBUG) -def start(test_mode): - with Tempserver(test_mode) as server: +def start(test_mode, nowait): + with Tempserver(test_mode, nowait) as server: server.run() def main(): @@ -145,6 +146,7 @@ parser.add_argument('-D', '--daemon', action='store_true') parser.add_argument('-d', '--debug', action='store_true') parser.add_argument('-t', '--test', action='store_true') + parser.add_argument('--nowait', action='store_true') args = parser.parse_args() setup_logging(args.debug) @@ -205,10 +207,10 @@ logpath = os.path.join(os.path.dirname(__file__), 'tempserver.log') logf = open(logpath, 'a+') with daemon.DaemonContext(pidfile=pidf, stdout=logf, stderr = logf): - start(args.test) + start(args.test, args.nowait) else: with pidf: - start(args.test) + start(args.test, args.nowait) if __name__ == '__main__': main() diff -r d15dda1b1f76 -r 6bacd8ca9f8f py/test.py --- a/py/test.py Sat Jul 06 18:29:45 2019 +0800 +++ b/py/test.py Sat Jul 06 18:30:25 2019 +0800 @@ -1,4 +1,3 @@ -#!/usr/bin/env python2.7 import io import unittest @@ -7,7 +6,7 @@ class TestSensors(unittest.TestCase): def setUp(self): - self.sensors = sensor_ds18b20.DS18B20s(None) + self.sensors = sensor_ds18b20.SensorDS18B20(None) def test_sensors_regex(self): f1 = """6e 01 4b 46 7f ff 02 10 71 : crc=71 YES @@ -66,9 +65,7 @@ jsbuf = io.StringIO() self.params.overshoot_delay = 123 - self.params.save(f=jsbuf) - - s = jsbuf.getvalue() + s = self.params.save_string() self.assertTrue('"overshoot_delay": 123' in s, msg=s) unittest.main() diff -r d15dda1b1f76 -r 6bacd8ca9f8f py/uploader.py --- a/py/uploader.py Sat Jul 06 18:29:45 2019 +0800 +++ b/py/uploader.py Sat Jul 06 18:30:25 2019 +0800 @@ -15,6 +15,7 @@ class Uploader(object): def __init__(self, server): self.server = server + self.limitlog = utils.NotTooOften(600) @asyncio.coroutine def run(self): @@ -43,6 +44,9 @@ return tosend + class BadServerResponse(Exception): + pass + @asyncio.coroutine def send(self, tosend): js = json.dumps(tosend) @@ -55,7 +59,7 @@ r = yield from asyncio.wait_for(aiohttp.request('post', config.UPDATE_URL, data=send_data), 60) result = yield from asyncio.wait_for(r.text(), 60) if r.status == 200 and result != 'OK': - raise Exception("Server returned %s" % result) + raise BadServerResponse("Server returned %s" % result) @asyncio.coroutine def do(self): @@ -67,6 +71,12 @@ yield from self.send(tosend) readings = None D("Sent updated %d readings" % nreadings) + except aiohttp.errors.ClientError as e: + self.limitlog.log("Error with uploader: %s" % str(e)) + except asyncio.TimeoutError as e: + self.limitlog.log("uploader http timed out: %s" % str(e)) + except self.BadServerResponse as e: + self.limitlog.log("Bad reply with uploader: %s" % str(e)) except Exception as e: EX("Error in uploader: %s" % str(e)) finally: diff -r d15dda1b1f76 -r 6bacd8ca9f8f py/utils.py --- a/py/utils.py Sat Jul 06 18:29:45 2019 +0800 +++ b/py/utils.py Sat Jul 06 18:30:25 2019 +0800 @@ -1,11 +1,13 @@ import os import sys -#import ctypes +import ctypes import time import select import logging import binascii import json +import datetime +import collections D = logging.debug L = logging.info @@ -138,3 +140,135 @@ def json_load_round_float(s, **args): return json.loads(s,parse_float = lambda f: round(float(f), 2), **args) + +class NotTooOften(object): + """ prevents things happening more than once per limit. + Isn't monotonic, good enough for logging. eg + self.logfailure = NotTooOften(180) # 3 minutes + ... + if self.logfailure(): + L("blah") + """ + def __init__(self, limit): + """ limit is a delay in seconds or TimeDelta """ + if type(limit) is datetime.timedelta: + self.limit = limit + else: + self.limit = datetime.timedelta(seconds=limit) + + # must be positive + assert self.limit > datetime.timedelta(0) + self.last = datetime.datetime(10, 1, 1) + + def __call__(self): + if datetime.datetime.now() - self.last > self.limit: + self.last = datetime.datetime.now() + return True + + def log(self, msg): + """ calls L(msg) if it isn't too often, otherwise D(msg) + """ + if self(): + L(msg + " (log interval %s)" % str(self.limit)) + else: + D(msg) + +Period = collections.namedtuple('Period', 'start end') +class StepIntegrator(object): + """ + Takes on/off events and a monotonically increasing timefn. Returns the integral + of (now-limittime, now) over those events. + + >>> s = StepIntegrator(lambda: t, 40) + >>> t = 1 + >>> s.turn(1) + >>> t = 10 + >>> s.turn(0) + >>> t = 20 + >>> s.turn(1) + >>> t = 30 + >>> print(s.integrate()) + 19 + >>> s.turn(0) + >>> print(s.integrate()) + 19 + >>> t = 35 + >>> print(s.integrate()) + 19 + >>> t = 42 + >>> print(s.integrate()) + 18 + >>> t = 52 + >>> print(s.integrate()) + 10 + >>> t = 69 + >>> print(s.integrate()) + 1 + >>> t = 70 + >>> print(s.integrate()) + 0 + >>> t = 170 + >>> print(s.integrate()) + 0 + """ + def __init__(self, timefn, limittime): + # _on_periods is a list of [period]. End is None if still on + self._on_periods = [] + self._timefn = timefn + self._limittime = limittime + + def set_limit(self, limittime): + if self._limittime == limittime: + return + self._limittime = limittime + self._trim() + + def turn(self, value): + if not self._on_periods: + if value: + self._on_periods.append(Period(self._timefn(), None)) + return + + # state hasn't changed + on_now = (self._on_periods[-1].end is None) + if value == on_now: + return + + if value: + self._on_periods.append(Period(self._timefn(), None)) + else: + self._on_periods[-1] = self._on_periods[-1]._replace(end = self._timefn()) + + def _trim(self): + begin = self._timefn() - self._limittime + # shortcut, first start is after begin + if not self._on_periods or self._on_periods[0].start >= begin: + return + + new_periods = [] + for s, e in self._on_periods: + if s == e: + continue + elif s >= begin: + new_periods.append(Period(s,e)) + elif e is not None and e < begin: + continue + else: + new_periods.append(Period(begin, e)) + self._on_periods = new_periods + + def integrate(self): + self._trim() + tot = 0 + for s, e in self._on_periods: + if e is None: + e = self._timefn() + tot += (e-s) + return tot + + + + + + + diff -r d15dda1b1f76 -r 6bacd8ca9f8f web/templog.py --- a/web/templog.py Sat Jul 06 18:29:45 2019 +0800 +++ b/web/templog.py Sat Jul 06 18:30:25 2019 +0800 @@ -132,11 +132,11 @@ + timedelta(minutes=(float(xpos) / (config.GRAPH_WIDTH * config.ZOOM)) * minutes) minutes = int(minutes / ZOOM_SCALE) - end = click_time + timedelta(minutes=minutes/2) + end = click_time + timedelta(minutes=minutes//2) else: # zoom out minutes = int(minutes*ZOOM_SCALE) - end += timedelta(minutes=minutes/2) + end += timedelta(minutes=minutes//2) if end > datetime.now(): end = datetime.now()