changeset 294:6bacd8ca9f8f

merge
author Matt Johnston <matt@ucc.asn.au>
date Sat, 06 Jul 2019 18:30:25 +0800
parents d15dda1b1f76 (current diff) 97d99eb42d27 (diff)
children 8441916e3095
files py/uploader.py web/templog.py
diffstat 10 files changed, 197 insertions(+), 38 deletions(-) [+]
line wrap: on
line diff
--- 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"
--- 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))
-
-
-
-
--- 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())
--- 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):
--- 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
--- 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()
--- 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()
--- 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:
--- 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
+
+
+
+
+
+
+
--- 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()