Summary graph: outlined areas, detailed legend.

New CSS design.
This commit is contained in:
François Rossigneux 2012-12-28 21:36:43 +01:00
parent 91761afaa1
commit 43d9a2c029
7 changed files with 180 additions and 179 deletions

View File

@ -18,8 +18,6 @@ def make_app():
app = flask.Flask(__name__)
app.register_blueprint(v1.blueprint)
# TODO not here
rrd.create_dirs()
thread.start_new_thread(rrd.listen, ())
@app.before_request

View File

@ -8,7 +8,6 @@ import itertools
import json
import os
import time
import threading
import uuid
import rrdtool
@ -52,8 +51,10 @@ scales['day'] = {'interval':86400, 'resolution':900, 'label': 'day'}, #
scales['week'] = {'interval':604800, 'resolution':7200, 'label': 'week'}, # Resolution = 2 hours
scales['month'] = {'interval':2678400, 'resolution':21600, 'label': 'month'}, # Resolution = 6 hours
scales['year'] = {'interval':31622400, 'resolution':604800, 'label': 'year'}, # Resolution = 1 week
colors = ['#EA644A', '#EC9D48', '#ECD748', '#54EC48', '#48C4EC', '#7648EC', '#DE48EC', '#8A8187']
probes = set()
probe_colors = {}
def create_dirs():
"""Creates all required directories."""
@ -98,70 +99,102 @@ def update_rrd(probe, watts):
filename = cfg.CONF.rrd_dir + '/' + str(uuid.uuid5(uuid.NAMESPACE_DNS, str(probe))) + '.rrd'
if not os.path.isfile(filename):
create_rrd_file(filename)
probes.add(probe)
try:
rrdtool.update(filename, 'N:%s' % watts)
except rrdtool.error, e:
LOG.error('Error updating RRD: %s' % e)
def build_summary_graph(scale):
"""Builds the summary graph."""
png_file = cfg.CONF.png_dir + '/' + scale + '/summary.png'
if scale in scales.keys() and len(probes) > 0:
def build_graph(scale, probe=None):
"""Builds the graph for the probe, or a summary graph."""
if scale in scales.keys() and len(probes) > 0 and (probe is None or probe in probes):
# Get PNG filename
if probe is not None:
png_file = get_png_filename(scale, probe)
else:
png_file = cfg.CONF.png_dir + '/' + scale + '/summary.png'
# Build required (PNG file not found or outdated)
if not os.path.exists(png_file) or os.path.getmtime(png_file) < time.time() - scales[scale][0]['resolution']:
args = [png_file,
'--start', '-' + str(scales[scale][0]['interval']),
if probe is not None:
# Specific arguments for probe graph
args = [png_file,
'--title', probe,
'--width', '497',
'--height', '187',
'--upper-limit', str(cfg.CONF.max_watts),
]
else:
# Specific arguments for summary graph
args = [png_file,
'--title', 'Summary',
'--width', '694',
'--height', '261',
]
# Common arguments
args += ['--start', '-' + str(scales[scale][0]['interval']),
'--end', 'now',
'--width', '694',
'--height', '261',
'--full-size-mode',
'--imgformat', 'PNG',
'--title', 'Summary (' + scales[scale][0]['label'] + ')',
'--alt-y-grid',
'--vertical-label', 'Watts',
'--lower-limit', '0',
'--rigid',
]
# Colors of the areas in the graph
colors = ['#EA644A', '#EC9D48', '#ECD748', '#54EC48', '#48C4EC', '#DE48EC', '#7648EC']
seq = itertools.cycle(colors)
cdef_kwh = 'CDEF:kwh='
cdef_cost = 'CDEF:cost='
for probe in probes:
rrd_file = get_rrd_filename(probe)
if os.path.exists(rrd_file):
# Data source
args.append('DEF:watt_with_unknown_%s=%s:w:AVERAGE' % (probe, rrd_file))
# Data source with unknown values set to zero
args.append('CDEF:watt_%s=watt_with_unknown_%s,UN,0,watt_with_unknown_%s,IF' % (probe, probe, probe))
# Real average (to compute kWh)
args.append('VDEF:wattavg_%s=watt_%s,AVERAGE' % (probe, probe))
# Compute kWh for the probe
# RPN expressions must contain DEF or CDEF variables, so we pop a CDEF value
args.append('CDEF:kwh_%s=watt_%s,POP,wattavg_%s,1000.0,/,%s,3600.0,/,*' % (probe, probe, probe, str(scales[scale][0]['interval'])))
# Compute cost
args.append('CDEF:cost_%s=kwh_%s,%f,*' % (probe, probe, cfg.CONF.kwh_price))
# Append kWh and cost to a CDEF expression
cdef_kwh += 'kwh_' + probe + ','
cdef_cost += 'cost_' + probe + ','
# Draw the area for the probe
args.append('AREA:watt_%s%s::STACK' % (probe, seq.next()))
# Prepare CDEF expression of kWh and cost by adding the required number of '+'
cdef_kwh += '+,' * int(len(probes)-2) + '+'
cdef_cost += '+,' * int(len(probes)-2) + '+'
# Distinguish the quantity of probes because CDEF expression is invalid if there is less than 2 probes
if len(probes) >= 2:
args.append(cdef_kwh)
args.append(cdef_cost)
# Legend
args.append('GPRINT:kwh:LAST:Total\: %lf kWh')
args.append('GPRINT:cost:LAST:Cost\: %lf')
if scale == 'minute':
args += ['--x-grid', 'SECOND:30:MINUTE:1:MINUTE:1:0:%H:%M']
cdef_watt = 'CDEF:watt='
cdef_watt_with_unknown = 'CDEF:watt_with_unknown='
graph_lines = []
stack = False
if probe is not None:
probe_list = [probe]
else:
# Legend
args.append('GPRINT:kwh_%s' % list(probes)[0] + ':LAST:Total\: %lf kWh')
args.append('GPRINT:cost_%s' % list(probes)[0] + ':LAST:Cost\: %lf')
probe_list = sorted(probes)
for probe in probe_list:
probe_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, probe)
rrd_file = get_rrd_filename(probe)
# Data source
args.append('DEF:watt_with_unknown_%s=%s:w:AVERAGE' % (probe_uuid, rrd_file))
# Data source without unknown values
args.append('CDEF:watt_%s=watt_with_unknown_%s,UN,0,watt_with_unknown_%s,IF' % (probe_uuid, probe_uuid, probe_uuid))
# Prepare CDEF expression of total watt consumption
cdef_watt += 'watt_%s,' % probe_uuid
cdef_watt_with_unknown += 'watt_with_unknown_%s,' % probe_uuid
# Draw the area for the probe
color = probe_colors[probe]
args.append('AREA:watt_with_unknown_%s%s::STACK' % (probe_uuid, color + 'AA'))
if not stack:
graph_lines.append('LINE:watt_with_unknown_%s%s::' % (probe_uuid, color))
stack = True
else:
graph_lines.append('LINE:watt_with_unknown_%s%s::STACK' % (probe_uuid, color))
if len(probe_list) >= 2:
# Prepare CDEF expression by adding the required number of '+'
cdef_watt += '+,' * int(len(probe_list)-2) + '+'
cdef_watt_with_unknown += '+,' * int(len(probe_list)-2) + '+'
args += graph_lines
args.append(cdef_watt)
args.append(cdef_watt_with_unknown)
# Min watt
args.append('VDEF:wattmin=watt_with_unknown,MINIMUM')
# Max watt
args.append('VDEF:wattmax=watt_with_unknown,MAXIMUM')
# Partial average that will be displayed (ignoring unknown values)
args.append('VDEF:wattavg_with_unknown=watt_with_unknown,AVERAGE')
# Real average (to compute kWh)
args.append('VDEF:wattavg=watt,AVERAGE')
# Compute kWh for the probe
# RPN expressions must contain DEF or CDEF variables, so we pop a CDEF value
args.append('CDEF:kwh=watt,POP,wattavg,1000.0,/,%s,3600.0,/,*' % str(scales[scale][0]['interval']))
# Compute cost
args.append('CDEF:cost=watt,POP,kwh,%f,*' % cfg.CONF.kwh_price)
# Legend
args.append('GPRINT:wattavg_with_unknown:Avg\: %3.1lf W')
args.append('GPRINT:wattmin:Min\: %3.1lf W')
args.append('GPRINT:wattmax:Max\: %3.1lf W')
args.append('GPRINT:watt_with_unknown:LAST:Last\: %3.1lf W\j')
args.append('TEXTALIGN:center')
args.append('GPRINT:kwh:LAST:Total\: %lf kWh')
args.append('GPRINT:cost:LAST:Cost\: %lf')
LOG.info('Build PNG summary graph')
rrdtool.graph(args)
return png_file
@ -169,58 +202,6 @@ def build_summary_graph(scale):
LOG.info('Retrieve PNG summary graph from cache')
return png_file
def build_graph(scale, probe):
"""Builds the graph for this probe."""
png_file = get_png_filename(scale, probe)
rrd_file = get_rrd_filename(probe)
if scale in scales.keys() and os.path.exists(rrd_file):
# Build required (PNG file not found or outdated)
if not os.path.exists(png_file) or os.path.getmtime(png_file) < time.time() - scales[scale][0]['resolution']:
LOG.info('Build PNG graph')
rrdtool.graph(png_file,
'--start', '-' + str(scales[scale][0]['interval']),
'--end', 'now',
'--upper-limit', str(cfg.CONF.max_watts),
'--imgformat', 'PNG',
# Data source
'DEF:watt_with_unknown=%s:w:AVERAGE' % rrd_file,
# Min watt
'VDEF:wattmin=watt_with_unknown,MINIMUM',
# Max watt
'VDEF:wattmax=watt_with_unknown,MAXIMUM',
# Data source with unknown values set to zero
'CDEF:watt=watt_with_unknown,UN,0,watt_with_unknown,IF',
# Partial average that will be displayed (ignoring unknown values)
'VDEF:wattavg_with_unknown=watt_with_unknown,AVERAGE',
# Real average (to compute kWh)
'VDEF:wattavg=watt,AVERAGE',
# Compute kWh for the probe
# RPN expressions must contain DEF or CDEF variables, so we pop a CDEF value
'CDEF:kwh=watt,POP,wattavg,1000.0,/,%s,3600.0,/,*' % str(scales[scale][0]['interval']),
# Compute cost
'CDEF:cost=watt,POP,kwh,%f,*' % cfg.CONF.kwh_price,
'--title', probe + ' (' + scales[scale][0]['label'] + ')',
'--vertical-label', 'Watts',
'--lower-limit', '0',
'--rigid',
# Draw the area and a line for the probe
'AREA:watt_with_unknown#0000FF22',
'LINE:watt_with_unknown#0000FFAA',
# Legend
'GPRINT:wattavg_with_unknown:Avg\: %3.1lf W',
'GPRINT:wattmin:Min\: %3.1lf W',
'GPRINT:wattmax:Max\: %3.1lf W',
'GPRINT:watt_with_unknown:LAST:Last\: %3.1lf W',
'GPRINT:kwh:LAST:Total\: %lf kWh',
'GPRINT:cost:LAST:Cost\: %lf',
)
else:
LOG.info('Retrieve PNG graph from cache')
return png_file
else:
LOG.warning('Probe or scale not found')
def listen():
"""Subscribes to ZeroMQ messages, and adds received measurements to the database.
Messages are dictionaries dumped in JSON format.
@ -228,6 +209,8 @@ def listen():
"""
LOG.info('RRD listenig to %s' % cfg.CONF.probes_endpoint)
create_dirs()
context = zmq.Context.instance()
subscriber = context.socket(zmq.SUB)
subscriber.setsockopt(zmq.SUBSCRIBE, '')
@ -243,6 +226,12 @@ def listen():
LOG.error('Bad message signature')
else:
try:
update_rrd(measurements['probe_id'].encode('utf-8'), float(measurements['w']))
probe = measurements['probe_id'].encode('utf-8')
update_rrd(probe, float(measurements['w']))
if not probe in probes:
probes.add(probe)
color_seq = itertools.cycle(colors)
for probe in sorted(probes):
probe_colors[probe] = color_seq.next()
except KeyError:
LOG.error('Malformed message (missing required key)')

View File

@ -1,82 +1,94 @@
body {
background-color: #333333;
font: 0.8em Arial;
margin: 20px auto;
min-width: 800px;
max-width: 1078px;
background-color:#E2E2E2;
font:0.8em Arial;
margin:20px auto;
max-width:1078px;
min-width:800px;
}
#header {
background-color: #692F4C;
color:#F5F5F5;
padding: 5px;
text-align: center;
line-height: 12px;
background-color:#3A5926;
background-image:url('/static/header.jpg');
background-position:0px -18px;
color:#F4F1E9;
padding:4px;
text-align:center;
}
#inner {
background-color: #F5F5F5;
border-color: #7D7D7D;
border-style: solid;
border-width: 0 0 8px 0;
padding: 20px;
background-color:white;
border-color:#A2A2A2;
border-style:solid;
border-width:0px 0px 8px 0px;
padding:20px;
}
.menu {
width:100%;
background:#333333;
overflow:hidden;
position:relative;
}
background:#505050;
background:-webkit-linear-gradient(#505050, #222222); /* Safari 5.1+, Chrome 10+ */
background:linear-gradient(#505050, #222222);
overflow:hidden;
position:relative;
width:100%;
}
.menu ul {
clear:left;
float:left;
list-style:none;
margin:0;
padding:0;
position:relative;
left:50%;
text-align:center;
clear:left;
float:left;
left:50%;
list-style:none;
margin:0;
padding:0;
position:relative;
text-align:center;
}
.menu ul li {
display:block;
float:left;
list-style:none;
margin:0;
padding:0;
position:relative;
right:50%;
display:block;
float:left;
list-style:none;
margin:0px 8px 0px 0px;
padding:0;
position:relative;
right:50%;
width:80px;
}
.menu ul li a {
display:block;
padding: 10.5px 11px;
color:#FFFFFF;
text-decoration:none;
font-family:Arial;
font-size:8pt;
font-weight:bold;
text-transform:uppercase;
letter-spacing:.08em;
border-radius: 8px 8px 0px 0px;
color:#E2E2E2;
display:block;
font-family:Arial;
font-size:8pt;
font-weight:bold;
letter-spacing:.08em;
padding:10.5px 11px;
text-decoration:none;
text-transform:uppercase;
}
.menu ul li a:hover,
.menu ul li a:hover:not(.active) {
background:#D0D0D0;
background:-webkit-linear-gradient(#D0D0D0, white); /* Safari 5.1+, Chrome 10+ */
background:linear-gradient(#D0D0D0, white);
color:#222222;
}
.menu ul li a.active {
background-color:#222222;
background-color:white;
color:#222222;
}
h2 {
font-weight:normal;
font-weight:normal;
}
img {
padding: 10px;
padding:10px;
}
#summary {
display: block;
display:block;
margin-left:auto;
margin-right:auto;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@ -1,44 +1,46 @@
<!doctype html>
<html lang="en">
<head>
<head>
<title>Kwapi monitoring</title>
<meta charset="utf-8">
<link rel="stylesheet" href="{{ url_for('v1.static', filename='design.css') }}"/>
<link rel="icon" type="image/png" href="{{ url_for('v1.static', filename='favicon.png') }}" />
<script type="text/javascript">
// <![CDATA[
function freshimg(image){
if (image.src.indexOf("?") == -1)
image.src = image.src + "?reload=" + Date.now();
function reloadImage(image)
{
if(image.src.indexOf("?") == -1)
image.src = image.src + "?reload=" + Date.now();
}
// ]]>
</script>
</head>
<body>
<div id="header"><h1>Kwapi energy monitoring</h1></div>
<!-- Horizontal menu bar -->
<div class="menu">
<ul>
{% for label in scales %}
{% if label == scale %}
<li><a class="active" href="/last/{{ label }}">{{ label }}</a></li>
{% else %}
<li><a href="/last/{{ label }}">{{ label }}</a></li>
{% endif %}
{% endfor %}
</ul>
</div>
<div id="inner">
<div id="header"><h1>Kwapi energy monitoring</h1></div>
<!-- Horizontal menu bar -->
<div class="menu">
<ul>
{% for label in scales %}
{% if label == scale %}
<li><a class="active" href="/last/{{ label }}">{{ label }}</a></li>
{% else %}
<li><a href="/last/{{ label }}">{{ label }}</a></li>
{% endif %}
{% endfor %}
</ul>
</div>
<!-- Scale view (all probes for one scale) -->
{% if view == 'scale' %}
{% if probe_amount > 0 %}
{% if probes|count > 0 %}
<h2>Summary</h2>
<!-- Display summary graph -->
<img id="summary" src="/graph/{{ scale }}" alt="Summary graph" onload="freshimg(this)"/>
<img id="summary" src="/graph/{{ scale }}" alt="Summary graph" onload="reloadImage(this)"/>
<h2>Details</h2>
<!-- Display all probe graphs -->
{% for probe in probes %}
<a href="/probe/{{ probe }}"><img src="/graph/{{ scale }}/{{ probe }}" alt="Graph {{ probe }}" onload="freshimg(this)"/></a>
<a href="/probe/{{ probe }}"><img src="/graph/{{ scale }}/{{ probe }}" alt="Graph {{ probe }}" onload="reloadImage(this)"/></a>
{% endfor %}
{% else %}
<p>No probes found.</p>
@ -47,7 +49,7 @@
<!-- Probe view (all scales for one probe) -->
{% elif view == 'probe' %}
{% for scale in scales %}
<img src="/graph/{{ scale }}/{{ probe }}" alt="Graph {{ probe }}" onload="freshimg(this)"/>
<img src="/graph/{{ scale }}/{{ probe }}" alt="Graph {{ probe }}" onload="reloadImage(this)"/>
{% endfor %}
{% endif %}
</div>

View File

@ -19,7 +19,7 @@ def welcome_scale(scale):
if scale not in flask.request.scales:
flask.abort(404)
try:
return flask.render_template('index.html', probes=flask.request.probes, scales=flask.request.scales, scale=scale, probe_amount=len(flask.request.probes), view='scale')
return flask.render_template('index.html', probes=sorted(flask.request.probes), scales=flask.request.scales, scale=scale, view='scale')
except TemplateNotFound:
flask.abort(404)
@ -36,7 +36,7 @@ def welcome_probe(probe):
def send_summary_graph(scale):
"""Sends summary graph."""
scale = scale.encode('utf-8')
png_file = rrd.build_summary_graph(scale)
png_file = rrd.build_graph(scale)
try:
return flask.send_file(png_file)
except: