Mercurial > templog
view web/log.py @ 284:8ea6c90774e3
add dependencies
author | Matt Johnston <matt@ucc.asn.au> |
---|---|
date | Wed, 23 Mar 2016 21:34:44 +0800 |
parents | 6c14e0573f50 |
children | 61269311ed3d |
line wrap: on
line source
# -*- coding: utf-8 -*- #:vim:et:ts=4:sts=4:sw=4: import rrdtool import os import os.path import sys import glob import hashlib import tempfile import time import syslog import sqlite3 import traceback import datetime import struct import binascii import json import subprocess from colorsys import hls_to_rgb import config import atomicfile import settings fridge_settings = settings.Settings() def sensor_rrd_path(s): return '%s/sensor_%s.rrd' % (config.DATA_PATH, str(s)) # returns (path, sensor_name) tuples def all_sensors(): return [(r, os.path.basename(r[:-4])) for r in glob.glob('%s/*.rrd' % config.DATA_PATH)] def create_rrd(sensor_id): # start date of 10 seconds into 1970 is used so that we can # update with prior values straight away. if 'voltage' in sensor_id: args = [ '--step', '3600', 'DS:temp:GAUGE:7200:1:10', 'RRA:AVERAGE:0.5:1:87600'] elif 'fridge_on' in sensor_id: args = [ '--step', '300', 'DS:temp:GAUGE:600:-100:500', 'RRA:LAST:0.5:1:1051200'] else: args = [ '--step', '300', 'DS:temp:GAUGE:600:-100:500', 'RRA:AVERAGE:0.5:1:1051200'] rrdtool.create(sensor_rrd_path(sensor_id), '--start', 'now-60d', *args) # stolen from viewmtn, stolen from monotone-viz def colour_from_string(str): def f(off): return ord(hashval[off]) / 256.0 hashval = hashlib.sha1(str).digest() hue = f(5) li = f(1) * 0.15 + 0.55 sat = f(2) * 0.5 + .5 return ''.join(["%.2x" % int(x * 256) for x in hls_to_rgb(hue, li, sat)]) def graph_png(start, length): os.environ['MATT_PNG_BODGE_COMPRESS'] = '4' os.environ['MATT_PNG_BODGE_FILTER'] = 'paeth' rrds = all_sensors() graph_args = [] have_volts = False ## volts = temp * volts_div + volts_shift #volts_div = 10 #volts_shift = 2 volts_div = 1 volts_shift = 0 volts_mult = 1.0/volts_div # (title, sensorline) pairs. sensor_lines = [] wort_sensor = None fridge_sensor = None for n, (rrdfile, sensor) in enumerate(rrds): unit = None if 'avrtemp' in sensor: continue if 'voltage' in sensor: have_volts = True vname = 'scalevolts' graph_args.append('DEF:%(vname)s=%(rrdfile)s:temp:AVERAGE:step=3600' % locals()) unit = 'V' elif 'fridge_on' in sensor: vname = 'fridge_on' graph_args.append('DEF:raw%(vname)s=%(rrdfile)s:temp:LAST' % locals()) graph_args.append('CDEF:%(vname)s=raw%(vname)s,-0.2,*,3,+' % locals()) else: vname = 'temp%d' % n graph_args.append('DEF:raw%(vname)s=%(rrdfile)s:temp:AVERAGE' % locals()) # limit max temp to 50 graph_args.append('CDEF:%(vname)s=raw%(vname)s,38,GT,UNKN,raw%(vname)s,%(volts_mult)f,*,%(volts_shift)f,+,IF' % locals()) unit = '<span face="Liberation Serif">º</span>C' format_last_value = None if unit: try: last_value = float(rrdtool.info(rrdfile)['ds[temp].last_ds']) format_last_value = ('%f' % last_value).rstrip('0').rstrip('.') + unit except ValueError: pass width = config.LINE_WIDTH legend = config.SENSOR_NAMES.get(sensor, sensor) colour = config.SENSOR_COLOURS.get(legend, colour_from_string(sensor)) print_legend = '' if legend in config.LEGEND_NAMES: if format_last_value: print_legend = ':%s (%s)' % (legend, format_last_value) else: print_legend = ":%s" % legend sensor_lines.append( (legend, 'LINE%(width)f:%(vname)s#%(colour)s%(print_legend)s' % locals()) ) if legend == 'Wort': wort_sensor = vname elif legend == 'Fridge': fridge_sensor = vname # calculated bits colour = '000000' graph_args.append('CDEF:wortdel=%(wort_sensor)s,PREV(%(wort_sensor)s),-' % locals()) graph_args.append('CDEF:tempdel=%(wort_sensor)s,%(fridge_sensor)s,-' % locals()) graph_args.append('CDEF:fermheat=wortdel,80,*,tempdel,0.9,*,+' % locals()) graph_args.append('CDEF:trendfermheat=fermheat,7200,TRENDNAN' % locals()) graph_args.append('CDEF:limitfermheat=trendfermheat,5,+,11,MIN,2,MAX' % locals()) graph_args.append('LINE0.5:limitfermheat#%(colour)s' % locals()) # lines are done afterwards so they can be layered sensor_lines.sort(key = lambda (legend, line): "Wort" in legend) graph_args += (line for (legend, line) in sensor_lines) #print>>sys.stderr, '\n'.join(graph_args) end = int(start+length) start = int(start) tempf = tempfile.NamedTemporaryFile() dateformat = '%H:%M:%S %Y-%m-%d' watermark = ("Now %s\t" "Start %s\t" "End %s" % ( datetime.datetime.now().strftime(dateformat), datetime.datetime.fromtimestamp(start).strftime(dateformat), datetime.datetime.fromtimestamp(end).strftime(dateformat) )) args = [tempf.name, '-s', str(start), '-e', str(end), '-w', str(config.GRAPH_WIDTH), '-h', str(config.GRAPH_HEIGHT), '--slope-mode', '--border', '0', # '--vertical-label', 'Voltage', '--y-grid', '%(volts_mult)f:1' % locals(), '--dynamic-labels', '--grid-dash', '1:0', '--zoom', str(config.ZOOM), '--color', 'GRID#00000000', '--color', 'MGRID#aaaaaa', '--color', 'BACK#ffffff', '--disable-rrdtool-tag', '--pango-markup', '--watermark', watermark, '--imgformat', 'PNG'] \ + graph_args args += ['--font', 'DEFAULT:12:%s' % config.GRAPH_FONT] args += ['--font', 'WATERMARK:10:%s' % config.GRAPH_FONT] if have_volts: volts_shift_div = volts_div * volts_shift args += ['--right-axis', '%(volts_div)f:-%(volts_shift_div)f' % locals(), # '--right-axis-format', '%.0lf', # '--right-axis-label', 'Temperature' ] #print>>sys.stderr, ' '.join("'%s'" % s for s in args) rrdtool.graph(*args) #return tempf return tempf.read() def validate_value(m): if m == 85: return 'U' else: return '%f' % m def sensor_update(sensor_id, measurements): try: open(sensor_rrd_path(sensor_id)) except IOError, e: create_rrd(sensor_id) if measurements: values = ['%d:%s' % (t, validate_value(m)) for (t, m) in measurements] rrdfile = sensor_rrd_path(sensor_id) # XXX what to do here when it fails... for v in values: try: rrdtool.update(rrdfile, v) except rrdtool.error, e: print>>sys.stderr, "Bad rrdtool update '%s': %s" % (v, str(e)) traceback.print_exc(file=sys.stderr) # be paranoid #f = file(rrdfile) #os.fsync(f.fileno()) def debug_file(mode='r'): return open('%s/debug.log' % config.DATA_PATH, mode) def record_debug(params): f = debug_file('a+') f.write('===== %s =====\n' % time.strftime('%a, %d %b %Y %H:%M:%S')) json.dump(params, f, sort_keys=True, indent=4) f.flush() return f def tail_debug_log(): f = debug_file() f.seek(0, 2) size = f.tell() f.seek(max(0, size-30000)) return '\n'.join(l.strip() for l in f.readlines()[-400:]) def convert_ds18b20_12bit(reading): value = struct.unpack('>h', binascii.unhexlify(reading))[0] return value * 0.0625 def time_rem(name, entries): val_ticks = int(entries[name]) val_rem = int(entries['%s_rem' % name]) tick_wake = int(entries['tick_wake']) + 1 tick_secs = int(entries['tick_secs']) return val_ticks + float(val_rem) * tick_secs / tick_wake def write_current_params(current_params, current_epoch): fridge_settings.update(current_params, current_epoch) def read_current_params(): params, epochtag = fridge_settings.get() return params def parse(params): start_time = time.time() debugf = record_debug(params) remote_now = params['now'] time_diff = start_time - remote_now # readings is [ ({sensorname: value, ...}, time), ... ] readings = params['readings'] # measurements is {sensorname: [(time, value), ...], ...} measurements = {} for rs, t in readings: real_t = t + time_diff for s, v in rs.iteritems(): measurements.setdefault(s, []).append((real_t, v)) # one-off measurements here current_params = params['current_params'] current_epoch = params['current_params_epoch'] measurements['fridge_on'] = [ (time.time(), params['fridge_on']) ] measurements['fridge_setpoint'] = [ (time.time(), current_params['fridge_setpoint']) ] write_current_params(current_params, current_epoch) for s, vs in measurements.iteritems(): sensor_update(s, vs) timedelta = time.time() - start_time debugf.write("Updated sensors in %.2f secs\n" % timedelta) debugf.flush() # types used here define the type of a field _FIELD_DEFAULTS = { 'fridge_setpoint': 16.0, 'fridge_difference': 0.2, 'overshoot_delay': 720, # 12 minutes 'overshoot_factor': 1.0, # ºC 'disabled': False, 'nowort': True, 'fridge_range_lower': 3, 'fridge_range_upper': 3, } def get_params(): """ Can return None if there aren't any parameters yet, otherwise returns the parameter list """ r = [] vals = read_current_params() if not vals: return None for k, v in _FIELD_DEFAULTS.iteritems(): n = {'name': k, 'value': type(v)(vals[k])} if type(v) is bool: kind = 'yesno' else: kind = 'number' if k == 'overshoot_delay': n['unit'] = ' sec' n['amount'] = 60 n['digits'] = 0; else: n['unit'] = 'º' n['amount'] = 0.1; n['digits'] = 1; n['kind'] = kind n['title'] = k r.append(n) return json.dumps(r, sort_keys=True, indent=4) def send_params(params): # 'templog_receive' is ignored due to authorized_keys # restrictions. the rpi has authorized_keys with # command="/home/matt/templog/venv/bin/python /home/matt/templog/py/receive.py",no-pty,no-port-forwarding,no-x11-forwarding,no-agent-forwarding ssh-rsa AAAAB3NzaC.... args = [config.SSH_PROG, '-i', config.SSH_KEYFILE, config.SSH_HOST, 'templog_receive'] try: p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) (out, err) = p.communicate(json.dumps(params)) except OSError, e: print>>sys.stderr, e return "Failed update" if 'Good Update' in out: return True print>>sys.stderr, "Strange return from update:" print>>sys.stderr, out return "Unexpected update result" def same_type(a, b): ta = type(a) tb = type(b) if ta == int: ta = float if tb == int: tb = float return (ta == tb) def update_params(p): params = {} for i in p: params[i['name']] = i['value'] if params.viewkeys() != _FIELD_DEFAULTS.viewkeys(): diff = params.viewkeys() ^ _FIELD_DEFAULTS.viewkeys() return "Key mismatch, difference %s" % str(diff) for k, v in params.items(): if not same_type(v, _FIELD_DEFAULTS[k]): return "Bad type for %s, %s vs %s" % (k , type(v), type(_FIELD_DEFAULTS[k])) fridge_settings.update(params) return True