HowTo: Using RRD - Round Robin Database

I'm using a Yun as a simple controller to run low voltage AC lighting on a schedule. It also monitors the current consumption of the lights to verify proper control, and determine whether any bulbs are burned out. The sketch is a dumb I/O controller that receives on/off commands from a Python script, and sends analog readings to the script. All of the intelligence and control is in the Python code. There is also a website to control and monitor the system using the Bottle framework.

The Python code manages the timing and the on/off schedule, and converts the analog readings to Amps and condenses them into average values over a 5 second period. This is written to a SQLlite database, which is growing rapidly due to the high data volume.

I want a way to be able to see that data at high resolution while I'm working on the system and tracking down wiring faults or burned out lights. But I also want to look at the data over longer periods to be able to determine average bulb life and overall system reliability. For the short term analysis, I need the data at high resolution, but for the longer term views the data could be averaged over longer intervals. (When looking at a year's worth of data, having resolution down to 5 seconds is serious overkill!) I needed to have a way of getting high resolution for the short term, yet still cover a long period of time without storing excessive data.

As it turns out, there is a perfect solution: RRDtool. The "RRD" stands for Round Robin Database, which uses a scheme where data is logically organized as fixed size circular buffers: as new data comes in, the oldest data is overwritten. What makes it interesting is that you can have several different archives of the same data, all using different resolutions. So, by storing a single data value every five seconds, RRDtool automatically manages several circular buffers of data, for example:

  • Every 5 seconds for a day
  • Every minute for a week
  • Every hour for a year
  • Every day for 20 years

Of course, the resolution and time spans are completely configurable, those are just the values I'm using. It's still probably a lot more data than I really need, but it's far better than storing every sample forever.

I had used RRD in the past, in the context of collectd. That's a nice system, and it lets you collect and merge data from a lot of systems across a network, but it seemed to be overkill for this case. I wanted to figure out how to write to RRD directly without going through collectd. Documentation is out there, but it took me a while to figure out a streamlined method, so I figured I would share the results of my research here.

To get started, install the required software into OpenWRT:

opkg update
opkg install rrdtool pyrrd

This takes a while, as it installs lots of dependencies. It ends up installing:

  • rrdtool
  • librrd
  • libart
  • libfreetype
  • libbz2
  • libpng
  • pyrrd

The next step is to create the database. In this case, I have three data sources: an "On" flag (0=off or 1=on) to say whether the lights are on, the current consumption in Amps, and a "Status" flag (0=OK, 1=Warning, 2=Failure) to indicate whether the current consumption is less than expected. For each value, I want to track the minimum, maximum, and average values over the various time periods.

I won't go into the details of the commands to create the database, they are documented at http://www.rrdtool.org. I found the RRDtool Wizard helpful in working out the code to create the table, which I put in a shell script file:

#!/bin/ash

# Create an RRD database
#    3 data sources:
#        On:      GAUGE, min=0, max=1
#        Current: GAUGE, min=0, max=unknown
#        Status:  GAUGE, min=0, max=2
#    12 total RRAs:
#        4 RRA periods:
#            Every 5 seconds for a day = 17280 points, taken every sample
#            Every minute for a week   = 10080 points, taken every 12 samples
#            Every hour for a year     = 8760 points, taken every 720 samples
#            Every day for 2 decades   = 7300 points, taken every 17280 samples
#        3 RRA CFs:
#            MIN
#            MAX
#            AVERAGE

rrdtool create /mnt/sda1/lightingController/data/statistics.rrd \
--step '5' \
'DS:On:GAUGE:15:0:1' \
'DS:Current:GAUGE:15:0:U' \
'DS:Status:GAUGE:15:0:2' \
'RRA:AVERAGE:0.5:1:17280' \
'RRA:MIN:0.5:1:17280' \
'RRA:MAX:0.5:1:17280' \
'RRA:AVERAGE:0.5:12:10080' \
'RRA:MIN:0.5:12:10080' \
'RRA:MAX:0.5:12:10080' \
'RRA:AVERAGE:0.5:720:8760' \
'RRA:MIN:0.5:720:8760' \
'RRA:MAX:0.5:720:8760' \
'RRA:AVERAGE:0.5:17280:7300' \
'RRA:MIN:0.5:17280:7300' \
'RRA:MAX:0.5:17280:7300'

With the database created, it's time to start adding data. As mentioned in the beginning, I already had a Python script that was getting data from the sketch, and averaging the data into five second chunks. All that's needed at this point is to feed the data to RRD instead of SQLlite.

I'm using PyRRD to neatly wrap up the interface to the RRD file. Most of the examples I found are self-contained in that they create the database, stuff some data into it, and then plot it. That's nice to have it all in one place, but I don't want to re-create the database every time the script starts - I want it to keep appending data to the existing database. I already have the database created in by the above RRDtool commands, I didn't bother to re-create that same code using the PyRRD methods. So I needed to figure out how to simply append data to an existing database. As it turns out, it's quite simple, but you have to read the source code to find the answers. "Use the Source, Luke!"

The first step is to simply open the existing database:

from pyrrd.rrd import RRD
rrd = RRD('/mnt/sda1/lightingController/data/statistics.rrd')

The PyRRD objects are smart enough to read the structure from the database file and figure out everything it needs to know without having to go through all of the work of mirroring the structure in the Python code. Very nice. With the database open, all that's needed is to write data to the database. A series of values can be appended, and then committed to database at once, and that's handy if you're reading in a batch of samples. But in this case, with the data being generated live, I just wanted to append one sample at a time and commit the data at each sample. I created a simple function to do so:

# Write a sample to the RRD database
#
# Parameters:
#   rrd        - The RRD object to receive the data, assumed to already be open
#   on         - Boolean flag indicating whether the light is on
#   current    - Float value representing the current consumption, in Amps
#   status     - Flag to indicate light status, 0=OK, 1=Warning, 2=Failure
def updateRRD(rrd, on, current, status):
   # The bufferValue() parameter order is the same as in the RRD file definition
   rrd.bufferValue(time.time(), on, current, status)
   rrd.update()

Putting it all together, the general form of the Python script is:

#!/usr/bin/python

import time
from pyrrd.rrd import RRD


def updateRRD(rrd, on, current, status):
   rrd.bufferValue(time.time(), on, current, status)
   rrd.update()


# Open the existing RRD file
rrd = RRD('/mnt/sda1/lightingController/data/statistics.rrd')

# Repeat forever
while (True):

   # Collect data for a period of five seconds, and update the variables
   # Just using dummy assignments here, since the actual code is
   # project specific and out of scope of this post
   on = 1
   current = 2.34
   status = 0

   # Update the RRD file
   updateRRD(rrd, on, current, status)

This is now getting the data into my RRD file. A very simple way to verify data is going in is to use the lastupdate command from RRDtool, which prints out the last update time (seconds since Unix epoch of 1-Jan-1970) plus the last stored values:

rrdtool lastupdate /mnt/sda1/lightingController/data/statistics.rrd

The next step is to let this run for a while and collect some data, and then experiment with ways to plot the data by showing the graphs on the website managed by the Bottle framework application. This is still a work in progress, more to come...

That sounds interesting and useful.

I'm not clear whether RRD uses SQlite or is quite separate from it?

...R

RRD is completely unrelated to SQLite. I only mentioned SQLite as that is what the project was previously using.

The issue is that to store data that is useful for the short term, the data needs to be stored frequently. But storing data frequently over a long period of time requires a lot of data to be stored. But high resolution data from long ago is not necessarily important.

For example, when testing the system, and trying to track down a loose connection or burned out bulb, I want to look at the data at a high resolution (every 5 seconds in this case) but for a short period of time (perhaps only 5 or 10 minutes.)

But when looking at historical data, for example, how many times did I have a burned out bulb (and was there a pattern to it) over the last couple of years, I certainly don't need data at a 5 second resolution, a once a day average is probably plenty of resolution. (Two years at once per day is 730 data points, but at 5 second resolution it's over 12 million data points!)

To get a long term graph, I don't want to have to store 12 million data points, and I certainly don't want to iterate through all of them to get the few hundred pixels that will be plotted on a graph. It's just too much data. But I don't want to reduce the overall data rate to once per day, as that would be useless for short term analysis.

So RRD is the solution. You feed it data at a regular rate, and it manages several archives automatically. Each archive can have a different sample rate, and different maximum number of points.

This is completely unrelated to SQLite. You could do the same thing with SQLite, but would have to do it manually: maintain several different data tables, compute running averages for each table and update the tables at the proper rates, and monitor how many rows are in each table and delete old rows as necessary.

That's far too much work, RRDtool does that all automatically! Plus, it provides some nice graphing functions that generate image files of the data. My plan is to integrate that with the Bottle application, so that when a page is requested, it runs some Python code to get PyRRD to generate an image file, and then the Bottle application serves up a web page that references that image file. That's my next step, now that the system is starting to collect some data and I have some data to play with.

Bookmarked

sonnyyu:
Bookmarked

Thanks! That means a lot coming from you. 8)

Just a teaser...

This is a quick test of RRDtool's graphing ability showing the direction I'm heading. This is displaying the last 5 minutes of data, as I'm playing with the state of the system while it's collecting data every five seconds.

The blue line represents the average current draw. The area under the line is shaded when the the output is on, and blank when the output is off. (At the start, it shows I was drawing current even though the relay is off - it's faked on the breadboard, but in real life it would be a good indication of stuck relay contacts.) The color of the shading under the current draw plot indicates the status that the controller code determined by comparing the current to threshold values: green if it's good, yellow if it's getting low, and red if there is a definite problem. I will set the thresholds so that OK indicates all bulbs good, Warning is one bulb out, or failure is more than one bulb out.

The RRDtool command to generate the graph is:

rrdtool graph test.png                                            \
   --end now --start now-300                                      \
   --vertical-label Amps                                          \
   DEF:on=statistics.rrd:On:MAX                                   \
   DEF:status=statistics.rrd:Status:MAX                           \
   DEF:current=statistics.rrd:Current:AVERAGE                     \
   CDEF:ok=on,status,0,EQ,current,0,IF,0,IF                       \
   CDEF:warn=on,status,0,GT,status,1,LE,*,current,0,IF,0,IF       \
   CDEF:fail=on,status,1,GT,current,0,IF,0,IF                     \
   AREA:ok#00FF00:"OK"                                            \
   AREA:warn#FFFF00:"Warning"                                     \
   AREA:fail#FF0000:"Failure"                                     \
   LINE2:current#0000FF:"Current"

The first three DEF statements extract the basic data streams from the database. The CDEF statements calculate some additional values.

The RPN notation can be tricky to read and master, because it is a different way of thinking. Fortunately, it comes pretty easy to me because my favorite calculator is an HP 16-C programmer's calculator that uses RPN. I bought it over 30 years ago when it first came out while I was attending my university. I'm going to be real bummed out if it ever dies...

But back to the CDEF statements: the first one generates the values for the filled area when the status is OK. The logic can be thought of as:

if (on != 0)
{
   if (status == 0)
      on = current
   else
      on = 0
}
else
   on = 0

The second one is very similar, and can be read as:

if (on != 0)
{
   if ((status > 0) AND (status <=1))
      warn = current
   else
      warn = 0
}
else
   warn = 0

The third one is more like the first, where the value is the measured current if the system is on, and the status > 1.

All of the CDEFs use range checking, in case I want to also plot those values as lines and I change the corresponding DEFs to use AVERAGE instead of MAX. That would give me an indication of how long the error situation applied during a plotted interval, which might be interesting when showing a long term graph.

Finally, the AREA statements add the three filled in colors to the chart, giving them all names for the legend. Each area rises to the current if that combination of On and Status applies, or will be zero if it doesn't. This helps prevent issues with overlapping colors. Finally, the LINE statement plots the unmodified current as a line - it occurs last so it is on top of everything else and not obscured by the filled in areas.

That will be my basic plot style, with selections to be able to view the data over the last 5 minutes, or the last hour, day, week, month, year, or all data. As I get farther into it, I will likely add more features to select a custom display range and other formats.

Now that graphing is starting to make sense, it's time to figure out how to do it with Python at run time using PyRRD...

ShapeShifter:
RRD is completely unrelated to SQLite. I only mentioned SQLite as that is what the project was previously using.

The issue is that to store data that is useful for the short term, the data needs to be stored frequently. But storing data frequently over a long period of time requires a lot of data to be stored. But high resolution data from long ago is not necessarily important.

I understand the concept of RRD from your Original Post. I just wondered if it uses SQlite as the place to store its data. Clearly it does not. Presumably you could do the same sort of thing using an SQLite database.

I like SQlite (and MySql) because it is easy to access the data using other programs. For example I use SQliteMan to have a quick look at data and to change small bits and pieces without needing to write Python code.

...R

Sure, you could do the same sort of thing with any database, including SQLite. But you'll end up writing a lot of code to consolidate and manage the data, which brings in lots of possibilities for errors.

It's just like you could skip SQLlite and do all of the data storage and updates yourself using raw flat binary files and your own data structures.

Sure, it can be done. But if there is a tool out there that already does it, and does it well, why reinvent it?

ShapeShifter:
Sure, it can be done. But if there is a tool out there that already does it, and does it well, why reinvent it?

I think you are making too much of my comments. I was really trying to say "I would have liked it better if RRD used SQLite"

...R

OK, the last big piece of the puzzle...

This RRDtool command was posted above, which gives the chart at the beginning of reply #5:

rrdtool graph test.png                                            \
   --end now --start now-300                                      \
   --vertical-label Amps                                          \
   DEF:on=statistics.rrd:On:MAX                                   \
   DEF:status=statistics.rrd:Status:MAX                           \
   DEF:current=statistics.rrd:Current:AVERAGE                     \
   CDEF:ok=on,status,0,EQ,current,0,IF,0,IF                       \
   CDEF:warn=on,status,0,GT,status,1,LE,*,current,0,IF,0,IF       \
   CDEF:fail=on,status,1,GT,current,0,IF,0,IF                     \
   AREA:ok#00FF00:"OK"                                            \
   AREA:warn#FFFF00:"Warning"                                     \
   AREA:fail#FF0000:"Failure"                                     \
   LINE2:current#0000FF:"Current"

To generate this graph from Python, one could spawn a system process to run that as a command line. But this is how I figured out to do the same thing directly in Python using PyRRD:

from pyrrd.graph import DEF, CDEF, AREA, LINE, Graph
import time

FILENAME = '/mnt/sda1/lightingController/data/statistics.rrd'


# Draw a graph of the the current consumption with shading that shows status.
#
# Parameters:
#    duration   - a timedelta object giving the graph time period (ending now)
#    title      - the title for the graph
#    filename   - output filename for the graph
def drawCurrentStatusGraph(duration, title, filename):

   def1 = DEF(rrdfile=FILENAME, vname='on',      dsName='On',      cdef='MAX')
   def2 = DEF(rrdfile=FILENAME, vname='status',  dsName='Status',  cdef='MAX')
   def3 = DEF(rrdfile=FILENAME, vname='current', dsName='Current', cdef='AVERAGE')

   cdef1 = CDEF(vname='ok',   rpn='on,status,0,EQ,current,0,IF,0,IF')
   cdef2 = CDEF(vname='warn', rpn='on,status,0,GT,status,1,LE,*,current,0,IF,0,IF')
   cdef3 = CDEF(vname='fail', rpn='on,status,1,GT,current,0,IF,0,IF')

   area1 = AREA(defObj=cdef1, color='#00FF00', legend='OK')
   area2 = AREA(defObj=cdef2, color='#FFFF00', legend='Warning')
   area3 = AREA(defObj=cdef3, color='#FF0000', legend='Failure')
   line1 = LINE(defObj=def3,  color='#0000FF', legend='Current', width=2)

   g = Graph(filename, start=int(time.time())-duration, end=int(time.time()), vertical_label="Amps")
   g.data.extend([def1, def2, def3, cdef1, cdef2, cdef3, area1, area2, area3, line1])
   g.write()

For the most part, it's a line by line translation of the RRDtool command into a series of objects using various constructor defined by PyRRD. Once all of the DEFs, CDEFs, AREAs, and LINEs are created, a Graph object is created to define the general graph characteristics. Then, the data elements are given to the graph object, and finally the write() method is called to generate the actual file.

To generate the same image file as in reply #5, I can call this function like this:

drawCurrentStatusGraph(300, "Last 5 Minutes", "test.png")

OK, it's not the exact same image - this function adds a title to the graph ("Last 5 Minutes") which appears above the plot area. I added that embellishment, and will likely add others as I further refine the code (like being able to define the size of the image file (by adding width and height values to the Graph() constructor call.)

And of course, once you have the image file, it's an easy step to show it on a web page using standard web page techniques.

Robin2:
I think you are making too much of my comments. I was really trying to say "I would have liked it better if RRD used SQLite"

I understood that part, I was keying off of this sentence:

Presumably you could do the same sort of thing using an SQLite database.

I suppose it could've used an SQLite back end. or one could come up with a system that does use it. It could be an interesting hybrid system, and simplify querying the data off-line.

RRDtool does have the ability to do certain queries on the data, and can even export the entire database into an XML file for any sort of off-line processing. Of course, while it will let you look at and retrieve the data, you won't be able to use SQLiteMan to access it. :-\

Very cool! have it bookmarked now.

FWIW: It's going to 32F tonight in El Paso, Texas.
First snow stuck to the ground around 6pm, about 4 hours ago.
We are expecting a low of 28F at around 6am.
It was supposedly 16F this morning.

Snow is relatively normal this time of the year here.
It's strange to deal with the 100+F during the dry summer,
then flash flood in autumn, snow after the equinox.

Cheers
Jesse

Yes, the weather has been strange this fall, but I'm facing the opposite situation as you. I'm near Buffalo, NY (actually in the ski country snow belt South of the city) and this is what the view out my back door looked like around this time the past few years:

But right now it's 52F and raining, and all I see out there is wet. We did have some snow a couple weeks ago, about an inch of accumulation, but it was gone the next day when it warmed up and rained again. So far this winter, I've had to get out the heavy warm jacket only that one day. We've been breaking high temperature records frequently, and set the all-time record for having the latest ever first snowfall of the season.

The skiers and snowmobilers are NOT happy...

Now, for the final piece of the puzzle, integrating it all with Bottle.

But first, I made a minor change to the drawCurrentStatusGraph() function in reply #9: I added width and height parameters to the function, and passed them through to the Graph() constructor by adding ", width=width, height=height" to the parameter list. Now, I can specify the desired size of the image.

So, I added that function to my Bottle app script, and then added this route decorator and function: (It's beyond the scope of this thread to explain how Bottle works, there are lots of good references and tutorials out there on the subject)

@route('/chart/<size>/<period>/<value>')
def do_chartSPV(size, period, value):

    # Figure out the size
    width = 400
    height = 100
    factor = 1.0

    if (size == 'xs'):
        factor = 0.5
    elif (size == 's'):
        factor = 0.75
    elif (size == 'm'):
        factor = 1.0
    elif (size == 'l'):
        factor = 2.0
    elif (size == 'xl'):
        factor = 3.0
    elif (size == 'xxl'):
        width = 1500
        height = 750

    width  = int(width  * factor)
    height = int(height * factor)

    # Make sure the value is an integer
    try:
        val = int(value)
    except:
        val = 1

    # Make sure the converted value is not zero or negative
    if (val < 1):
        val = 1

    # Decode the period value, convert it into a duration and a units label
    if (period == 'sec'):
        duration = timedelta(seconds=val)
        units = 'Second'
    elif (period == 'min'):
        duration = timedelta(minutes=val)
        units = 'Minute'
    elif (period == 'hr'):
        duration = timedelta(hours=val)
        units = 'Hour'
    elif (period == 'day'):
        duration = timedelta(days=val)
        units = 'Day'
    elif (period == 'wk'):
        duration = timedelta(weeks=val)
        units = 'Week'
    elif (period == 'mon'):
        duration = timedelta(days=val*31)
        units = 'Month'
    elif (period == 'yr'):
        duration = timedelta(days=val*365)
        units = 'Year'
    else:
        duration = timedelta(days=val)
        units = 'Day'
        period = 'day'

    # Format a title. If it covers just one period, title is simpley "Last <Units>"
    # But if more than one, the title becomes "Last <val> <Units>s"
    # For example, it could be "Last Day" or "Last 3 Days"
    if (val == 1):
        title = '"Last {}"'.format(units)
    else:
        title = '"Last {} {}s"'.format(val, units)

    # Format a filename based on period and val. For example 'day3.png' for the last 3 days.
    name = '{}{}.png'.format(period, val)

    # Generate the actual plot
    drawCurrentStatusGraph(duration, title, name, width, height)

    # Return the generated file, but turn off all cacheing so the chart is always current
    response = static_file(name, root=CHART_PATH)
    response.set_header("Cache-Control", "no-cache, no-store")
    return response

This lets me put an IMG tag in my HTML that allows retrieval of a graph based an a variety of time periods and sizes. The general format is in the route decorator: /chart/<size>/<period>/<value> where:

  • is a size tag:

  • 'xs' = extra small, 200 x 50

  • 's' = small, 300 x 75

  • 'm' = medium, 400 x 100 (the default RRDtool size)

  • 'l' = large, 800 x 200

  • 'xl' = extra large, 1200 x 300

  • 'xxl' = jumbo, 1500 x 750

  • is a time period tag:

  • 'sec' = seconds

  • 'min' = minutes

  • 'hr' = hours

  • 'day' = days

  • 'wk' = weeks

  • 'mon' = months

  • 'yr' = years

  • is an integer indicating how many time periods to cover

For example I can embed an image URLs of:

  • /chart/m/hr/6 to get a medium size chart covering the last six hours
  • /chart/l/day/1 to get a large chart covering the last day
  • /chart/s/wk/2 to get a small chart covering the last two weeks

I then added a couple more route decorators and functions to give short versions of the URLs:

@route('/chart/<size>/<period>')
def do_chartP(size, period):
    return do_chartSPV(size, period, 1)

@route('/chart/<period>')
def do_chartP(period):
    return do_chartSPV('m', period, 1)

The first function lets me use a simple URL like /chart/s/day to get a small chart that covers one day (the of 1 is assumed), or even /chart/day that will assume a medium and a of 1. In either case, the associated functions simply call the full featured function with some default values. I'm sure a person more experienced with Bottle can come up with a simpler way of doing that using a single function and three route decorators, or even a single decorator with some default values?

So with all of this I can embed some HTML in my status page that looks like this:

<a href="/chart/xl/hr"><img src="/chart/m/hr"></a>

and it will show a medium sized image in the current page, which when the image is clicked it opens up an extra large version of that image on a page all by itself.

I'm relatively new at Python programming, so this code my not look very Pythonic to those who are more experienced in the language. If anybody has any constructive suggestions on how to improve these functions, I'd like to hear them. For example, I don't particularly like the long if/elif/else statements to decode the size and period values, but Python has no case statements, and I question whether it's worth writing a special class to encapsulate the concepts and handle the translation? If you have a better way to do it, please let me know.

I hope this series of posts is helpful to someone. RRDtool seems like it's a good way to look at certain data and visualize changes to that data over time. By design, it only works with time ordered data, and cannot display anything other than time on the X axis - as such, it is not a general purpose database or data visualization program. It's not applicable to all data collection scenarios, but if you have some data that is sampled regularly, and you want to look at it over widely different time spans with appropriate data resolution, then it seems to be a good solution, and one that is not too difficult to integrate with Python.

It also has the advantage of being completely local to the Yun. There is no dependence on network connectivity (other than to be able to get the current time!) and it does not have to keep sending data to some IoT data service or Google Spreadsheet. You don't need to set up accounts anywhere, nor rely on some third party to keep offering a service and keep their servers running all of the time. (And if you are in a situation like me where Internet bandwidth is not unlimited and is expensive, it's nice that you're not using up a lot of bandwidth sending data and retrieving charts.)

Here's a picture of the front yard in El Paso, Texas.
Yes, El Paso is a desert town.

More image at this link.

That's me in the down jacket. Dad and Mom in last pictures.

Is that Dad with the cane? He looks cold.

Still no snow here, although we did have some freezing rain and ice the other day. The front yard is turning into a lake from all the rain. They say we could get some snow tomorrow and Friday - now I wish it could hold off a couple more days: seems like everyone forgets how to drive in the snow and it takes them a few snowfalls to remember. Having the first widespread snow on New Year's Eve is not good - the impaired crazy partiers are going to have a hard time. Seems like a good day to stay home and far away from the roads.

The prototype has been up and running for a few days, dutifully collecting data. There's now enough in the database to start seeing some interesting things in the charts. At this point, it's all simulated data, but I think the simulation is realistic enough. Instead of the relay for control, and the current transformer for current sensing, I'm using a very simple breadboard setup:

The relay control signal is just going to an LED so I can see what it's doing, and it also goes to the top of a potentiometer so I can divide down the 5V output signal to a lower analog voltage. I tied the top of the pot to the output rather than 5V so that the analog input goes to zero when the output is off, giving more realistic data. (And also lets me test my thresholding logic so it waits a few seconds after power on for the current readings to stabilize before declaring a failure - if I checked the current immediately after turning on the output, it would still read very low, and if the simulator always was reporting current I wouldn't be able to test that situation.)

Now, let's look at some sample charts.

Yesterday, when the "on" cycle started, I was playing with the current draw, testing my thesholding code and the new code I added to send a text message to my phone if the status should change to Warning or Failure. When the output turned on, the measured current was about 6.5 Amps, and I quickly turned it down to about 5.5 Amps so it triggers a Warning. A few minutes later, I turned it down further to about 2.5 Amps and triggered a Failure. A few minutes later it's back up to about 5.25 Amps and back to Warning stage. I took this chart snapshot very shortly after sunrise, when the light automatically turned off, and you can see that the current has dropped to zero right at the end of the chart.

You can also see a gap in the data yesterday evening: I had the system unplugged and powered down for a bit over an hour last night, so no data was collected during that time. Rather than assume zero values during that interval, which could skew average data, RRD is smart enough to know that there is no data there, and leaves a gap in the plots. I think that's a nice feature.

Looking at the last week of data, you can see how the nights (when the lights are on and current is being drawn) are so much longer than the days. You can also see how much earlier the light turned on the evening of the 27th, when compared to the days before and after. I was testing the manual override logic: through the Bottle application, I can send it manual override commands to permanently turn the light on or off regardless of the schedule, or temporarily turn it on or off but switch again at the next programmed cycle. What shows in the chart is that I gave it the temporary turn on command a couple hours before sunset so the light came on early, but it still turned off at the next scheduled time. A successful test.

You can also see how the data consolidation functions work. For the current, its showing the average value for a time period, but the warning and failure colors are triggered by the maximum status value. This way, if an issue is reported at any time during that interval, it will be visible. If the status values were averaged, the consolidated display value would be somewhere between the discrete levels, and short intervals of an error status value would not be as visible. In this week's plot, you can clearly see that there was a time when the red error status is visible at the beginning of the last day's cycle.

While small, the gap in the data is still visible in yesterday's cycle.

When looking back at the last month, the daily cycles are significantly squashed together, but you can still see the daily on/off periods. You can clearly see the yellow warning for the last cycle, and if you look closely you can also see the red interval at the beginning of yesterday's cycle. I guess most of it is being covered up by the thicker blue line which is drawn on top of the fills. I'll have to play with that - will a thinner line still be easy to see and will it obscure less of the underlying fill? I will also run another experiment where a short failure period happens later in the cycle where it won't be obscured by the vertical blue line at the beginning of the cycle. It should stand out much better then.

No gaps are visible in the data at this point. There is a parameter you can set in the database to control how a loss of data is propagated. By default, if more than half of the points in a consolidated interval are missing, the interval will be drawn as missing data. If more than half of the points are valid, they will be consolidated into a new data point, but of course the missing data will not affect the average. I didn't change that parameter when I created the database, so the default 50% is being used. At this scale, the missing data periods are less than half of the displayed interval, so no gaps are shown.

Since it's only been running a few days, there isn't much to see on the yearly plot. Although you can clearly see the red and yellow lines of the error status coming through properly. You can also see that the peak value on the plot is much lower than the week and month plots, which are skewed by the initial large values around noon of the 26th, where I was simulating some full scale readings to test the conversion logic. While that was a high peak value, it was short enough that it averaged down to about 7 Amps as the peak average value. Then, since the next few days had lower average currents, you can see the plot settling down to a bit over 5 Amps.

It should be interesting once at least half a year's data is collected. Now that we are past the Winter Solstice and the days are getting longer, I suspect that the average current will decrease over the next six months as the nights get shorter and the lights are on for a shorter percentage of the time. Then, after the Summer Solstice the average current should start increasing again. Once there is a full year's data, I expect the data to show a close approximation of a sine wave cycle as it tracks the day length variations.


So far, I'm quite pleased with the way this is turning out. I guess it's now time to order the current transformer and amplifier parts and start prototyping the actual analog circuitry. The potentiometer I'm currently using to simulate the analog input has been useful for software testing, but I'm about ready to move past that stage.

ShapeShifter:
Is that Dad with the cane? He looks cold.

::::SNIP::::

@ShapeShifter,

Yep, that's Dad with the cane. Mom after that.
Dad is cold, but I told him I'd send the picture to his granddaughters - one is visiting for the next few days.
My sisters and brothers also got a copy. We all know he is forget.
I might have something to do with that Coca-Cola and Golden Oreo Diet he decided on lately.

FWIW: I'm looking to be back in California again - soon.

Jesse

Just a quick update: the system has been running very well for a while (still using my simulated breadboard, I think it's time to finalize the board design - but work is currently taking too much of my free time.)

But I wanted to take a chance and show some updated graphs.

The last month's activity:

You can see it's been running rather regularly, except for a couple missing days a few weeks ago when I loaded a different sketch as a quick test to answer a forum question, and forgot to put the proper sketch back on the Yun. ::slight_smile:

The year's activity so far:

Here you can see that it's been running for a couple months. You can still see the yellow and red warning and fault areas from when I was doing the initial testing (as shown in the detail plots earlier in this thread.) You can also see a red line in the middle of January - this was a test where I manually introduced a fault for a few seconds. Even though the fault is very brief - a very small fraction of a pixel on this scale - it's still visible as a one pixel line because it is taking the maximum value over the consolidation interval. It's working exactly as I'd hoped it would!

You can also see the gap in the data at the end of January, the gap that shows up so clearly in the Month plot. There is a little spike in the data there, since the part that's missing is mostly off time - it goes from an on period of over 10 amps, to an on period of over 10 amps, so it is not averaged with much off time, which is what's bringing the daily average down in the rest of the plot.

Another interesting observation - just as I had predicted/hoped, the long term plot of the average daily current over time is showing the decline due to the change of the length of the days. As the days get longer and the nights get shorter, the average power consumption goes down, and it's clearly visible in the plot. In essence, the output is being Pulse Width Modulated with a very long 24 hour period. Cool! 8)