Skip to main content
replaced http://codereview.stackexchange.com/ with https://codereview.stackexchange.com/
Source Link

Last time I worked on a DHT11 sensorI worked on a DHT11 sensor and got a lot of feedback, this time I have added another sensor (the BMP180), stored both of them in MySQL, and am now displaying them to the web.

Last time I worked on a DHT11 sensor and got a lot of feedback, this time I have added another sensor (the BMP180), stored both of them in MySQL, and am now displaying them to the web.

Last time I worked on a DHT11 sensor and got a lot of feedback, this time I have added another sensor (the BMP180), stored both of them in MySQL, and am now displaying them to the web.

Tweeted twitter.com/StackCodeReview/status/837811461221191680
Source Link
Der Kommissar
  • 20.1k
  • 4
  • 68
  • 158

Display Sensor Data to Web

Last time I worked on a DHT11 sensor and got a lot of feedback, this time I have added another sensor (the BMP180), stored both of them in MySQL, and am now displaying them to the web.

The Raspberry Pi now has an HTTP server running on port 80, and I've built a small little UI to display all my sensor data.

Graphs and UI

So this is all provided by a few files, one of them is a JavaScript file, one is a CSS file, one is an HTML file, and the rest are python files.

We'll start with the JavaScript for the graphs. This is pretty simple and self explanatory, the biggest issue I have is I need to break this down further so that there's no duplication. This is graphs.js:

function buildGraph(svgTitle, table, title) {
    var svg = d3.select(svgTitle);
    var margin = {top: 20, right: 20, bottom: 30, left: 40};
    var width = +svg.attr("width") - margin.left - margin.right;
    var height = +svg.attr("height") - margin.top - margin.bottom;
    var g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    var parseTime = d3.timeParse("%Y-%m-%d %H:%M:%S");

    var x = d3.scaleTime().rangeRound([0, width]);
    var y = d3.scaleLinear().rangeRound([height, 0]);

    var line = d3.line()
        .curve(d3.curveMonotoneX)
        //.curve(d3.curveBasis)
        .x(function(d) { return x(d.Logged); })
        .y(function(d) { return y(d.Value); });

    var area = d3.area()
        .curve(d3.curveMonotoneX)
        //.curve(d3.curveBasis)
        .x(function(d) { return x(d.Logged); })
        .y1(function(d) { return y(d.Value); });

    d3.tsv("history.py?LogType=" + table, function(d) {
        d.Logged = parseTime(d.Logged);
        d.Value = +d.Value;
        return d;
    }, function(error, data) {
        if (error) throw error;

        x.domain(d3.extent(data, function(d) { return d.Logged; }));
        var yS = y.domain(d3.extent(data, function(d) { return d.Value; })).nice();

        area.y0(y(yS.domain()[0]));

        g.append("g")
            .attr("class", "x axis")
            .attr("transform", "translate(0," + height + ")")
            .call(d3.axisBottom(x).tickSize(-height));

        g.append("g")
            .attr("class", "y axis")
            .call(d3.axisLeft(y).tickSize(-width))
            .append("text")
            .attr("fill", "#000")
            .attr("transform", "rotate(-90)")
            .attr("y", 6)
            .attr("dy", "0.71em")
            .attr("text-anchor", "em")
            .text(title);

        g.select(".y")
            .select(".domain")
            .attr("transform", "translate(-1,0)");

        g.append("path")
            .datum(data)
            .attr("fill", "none")
            .attr("stroke", "steelblue")
            .attr("stroke-linejoin", "round")
            .attr("stroke-linecap", "round")
            .attr("stroke-width", 1.5)
            .attr("d", line);

        g.append("path")
            .datum(data)
            .attr("fill", "steelblue")
            .attr("fill-opacity", "0.3")
            .attr("stroke", "none")
            .attr("d", area);
    });
}

function updateGraph(svgTitle, table, title) {
    var svg = d3.select(svgTitle);
    var margin = {top: 20, right: 20, bottom: 30, left: 40};
    var width = +svg.attr("width") - margin.left - margin.right;
    var height = +svg.attr("height") - margin.top - margin.bottom;
    var g = svg.select("g");

    var parseTime = d3.timeParse("%Y-%m-%d %H:%M:%S");

    var x = d3.scaleTime().rangeRound([0, width]);
    var y = d3.scaleLinear().rangeRound([height, 0]);

    var line = d3.line()
        .curve(d3.curveMonotoneX)
        //.curve(d3.curveBasis)
        .x(function(d) { return x(d.Logged); })
        .y(function(d) { return y(d.Value); });

    var area = d3.area()
        .curve(d3.curveMonotoneX)
        //.curve(d3.curveBasis)
        .x(function(d) { return x(d.Logged); })
        .y1(function(d) { return y(d.Value); });

    d3.tsv("history.py?LogType=" + table, function(d) {
        d.Logged = parseTime(d.Logged);
        d.Value = +d.Value;
        return d;
    }, function(error, data) {
        if (error) throw error;

        x.domain(d3.extent(data, function(d) { return d.Logged; }));
        var yS = y.domain(d3.extent(data, function(d) { return d.Value; })).nice();

        area.y0(y(yS.domain()[0]));

        g.selectAll("path").remove();
        g.selectAll("g").remove();

        g.append("g")
            .attr("class", "x axis")
            .attr("transform", "translate(0," + height + ")")
            .call(d3.axisBottom(x).tickSize(-height));

        g.append("g")
            .attr("class", "y axis")
            .call(d3.axisLeft(y).tickSize(-width))
            .append("text")
            .attr("fill", "#000")
            .attr("transform", "rotate(-90)")
            .attr("y", 6)
            .attr("dy", "0.71em")
            .attr("text-anchor", "em")
            .text(title);

        g.select(".y")
            .select(".domain")
            .attr("transform", "translate(-1,0)");

        g.append("path")
            .datum(data)
            .attr("fill", "none")
            .attr("stroke", "steelblue")
            .attr("stroke-linejoin", "round")
            .attr("stroke-linecap", "round")
            .attr("stroke-width", 1.5)
            .attr("d", line);

        g.append("path")
            .datum(data)
            .attr("fill", "steelblue")
            .attr("fill-opacity", "0.3")
            .attr("stroke", "none")
            .attr("d", area);
    });
}

Next we have the index.html page, which is not actually loaded by the browser. It's the template for the index.py to load, though it doesn't have to be now. (Previously I was doing wonky things with it.)

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
        <link href='bootstrap/css/bootstrap.min.css' rel='stylesheet'>
        <link href="style.css" rel="stylesheet" />
        <script src='d3/d3.min.js'></script>
        <script src='graphs.js'></script>
</head>
<body>
    <div class="header">
        Last refresh: <span class="lastRefresh"></span>;
        Pi time: <span class="piTime"></span>;
        Next sensor pull: <span class="nextRefresh"></span> (<span class="nextRefreshSeconds"></span>)
    </div>
    <div class="current">
        Most recent readings (<span id="logValue"></span>):
        <span class="">
            Temp 1:
            <span id="temperatureValue"></span>F
        </span>
        <span class="">
            Humidity:
            <span id="humidityValue"></span>%
        </span>
        <span class="">
            Pressure:
            <span id="pressureValue"></span>inhg
        </span>
        <span class="">
            Temp 2:
            <span id="bmpTempValue"></span>F
        </span>
        <span class="">
            Altitude:
            <span id="altitudeValue"></span>ft
        </span>
        <span class="">
            Sea Level Pressure:
            <span id="seaPressureValue"></span>inhg
        </span>
    </div>
    <div class='graphBlock'>
        <svg class='temp' width='1260' height='240'></svg>
        <svg class='humid' width='1260' height='240'></svg>
        <svg class='pressure' width='1260' height='240'></svg>
        <svg class='bmpTemp' width='1260' height='240'></svg>
        <svg class='altitude' width='1260' height='240'></svg>
        <svg class='seaPressure' width='1260' height='240'></svg>
    </div>
    <script src='jquery.1.12.4.min.js'></script>
    <script src='bootstrap/js/bootstrap.min.js'></script>

    <script>
        Number.prototype.padLeft = function(base, chr) {
            var len = (String(base || 10).length - String(this).length) + 1;
            return len > 0 ? new Array(len).join(chr || '0') + this : this;
        }

        function round(value, decimals) {
            return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals);
        }

        function updateGraphs(callback) {
            if (callback == null) {
                callback = updateGraph;
            }

            callback('.temp', 'Temperature', 'Temp (F)');
            callback('.humid', 'Humidity', 'Humidity (%)');
            callback('.pressure', 'Pressure', 'Pressure (inhg)');
            callback('.bmpTemp', 'BmpTemp', 'Temp (F)');
            callback('.altitude', 'Altitude', 'Altitude (ft)');
            callback('.seaPressure', 'SeaPressure', 'Sea Level Pressure (inhg)');

            var d = new Date;
            var dFormat = [d.getUTCFullYear(), (d.getUTCMonth() + 1).padLeft(), d.getUTCDate().padLeft()].join('-');
            dFormat += " ";
            dFormat += [d.getUTCHours().padLeft(), d.getUTCMinutes().padLeft(), d.getUTCSeconds().padLeft()].join(':');
            $('.lastRefresh').html(dFormat);
        }

        function updateTimes() {
            updatePiTime();
        }

        function updatePiTime(target, params) {
            var xmlHttp = new XMLHttpRequest();
            xmlHttp.onreadystatechange = function() {
                if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
                    var text = xmlHttp.responseText.trim().split("\n");
                    $(".nextRefresh").html(text[0]);
                    $(".nextRefreshSeconds").html(text[1]);
                    $(".piTime").html(text[2]);
                }
            }
            xmlHttp.open("GET", "time.py?All=true", true);
            xmlHttp.send(null);
        }

        function updateCurrents() {
            var xmlHttp = new XMLHttpRequest();
            xmlHttp.onreadystatechange = function() {
                if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
                    var text = xmlHttp.responseText.trim().split("\n");
                    text.forEach(function(line) {
                        var items = line.split("\t");
                        switch (items[0]) {
                            case "Humidity":
                                $("#humidityValue").html(round(items[2], 2));
                                break;
                            case "BmpTemp":
                                $("#bmpTempValue").html(round(items[2], 2));
                                break;
                            case "Pressure":
                                $("#pressureValue").html(round(items[2], 2));
                                break;
                            case "Altitude":
                                $("#altitudeValue").html(round(items[2], 2));
                                break;
                            case "Temperature":
                                $("#temperatureValue").html(round(items[2], 2));
                                break;
                            case "SeaPressure":
                                $("#seaPressureValue").html(round(items[2], 2));
                                break;
                        }
                        $("#logValue").html(items[1]);
                    });
                }
            }
            xmlHttp.open("GET", "latest.py", true);
            xmlHttp.send(null);
        }

        updateGraphs(buildGraph);
        updateTimes();
        updateCurrents();
        setInterval(updateGraphs, 5000);
        setInterval(updateTimes, 500);
        setInterval(updateCurrents, 1000);
    </script>
</body>
</html>

As you can see, the root of the web page is JavaScript, that's to keep data updated so that page refreshes aren't required.

All the CSS code, which is pretty simple (style.css):

body {
    background: #202020;
    color: #efefef;
}

svg {
    display: inline-block;
}

.graphBlock {
    width: 100%;
    text-align: center;
}

.axis line {
    stroke: #303030;
}

.axis.y path,
.axis.y .tick:nth-child(2) line {
    stroke: #efefef;
}

.axis path {
    stroke: #303030;
}

.axis text {
    fill: #efefef;
}

.header {
    /*color: #efefef;*/
    background: #07507F;
    padding: 5px;
}

.current {
    /*color: #efefef;*/
    background: #07507F;
    padding: 5px;
    border-bottom: 1px solid #303030;
}

Then we have index.py, the main landing page:

#!/usr/bin/python
from datetime import datetime
import pymysql

# Now we do the rest of the work
if __name__ == "__main__":
    # Print the header first, always
    print "Content-type: text/html"
    print

    with open("index.html") as template:
        print template.read()

Then we have the asynchronously requested python scripts. The first is time.py since it's the shortest:

#!/usr/bin/python
from datetime import datetime, timedelta

import cgi


DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"


if __name__ == '__main__':
    print "Content-Type: text/html"
    print

    args = cgi.FieldStorage()
    date_format = DEFAULT_DATE_FORMAT
    if "DateFormat" in args:
        date_format = args["DateFormat"].value
    
    if "Next" in args or "All" in args:
        utctime = datetime.utcnow()
        print (utctime + timedelta(seconds = 60 - utctime.second)).strftime(date_format)
    if "NextIn" in args or "All" in args:
        utctime = datetime.utcnow()
        print str(60 - utctime.second) + " seconds"
    if (not "Next" in args and not "NextIn" in args) or "All" in args:
        print datetime.utcnow().strftime(date_format)

You can probably guess that it's only job is to return each time operation.

Next we have latest.py, which returns the latest value of each metric:

#!/usr/bin/python
from datetime import datetime, timedelta

import cgi
import pymysql


DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
DEFAULT_DAY_LIMIT = 7
DEFAULT_MINUTE_GROUPING = 1


if __name__ == '__main__':
    print "Content-type: text/html"
    print

    args = cgi.FieldStorage()
    start_date = (datetime.utcnow() - timedelta(days = DEFAULT_DAY_LIMIT)).strftime(DATE_FORMAT)
    end_date = datetime.utcnow().strftime(DATE_FORMAT)
    minute_group = DEFAULT_MINUTE_GROUPING
    gather = []

    gather.append(("HumidityLog", lambda x: x))
    gather.append(("BmpTempLog", lambda c: c * 1.8 + 32))
    gather.append(("PressureLog", lambda p: p * 0.0002953))
    gather.append(("AltitudeLog", lambda m: m * 3.28084))
    gather.append(("TemperatureLog", lambda c: c * 1.8 + 32))
    gather.append(("SeaPressureLog", lambda p: p * 0.0002953))

    if "Start" in args:
        start_date = args["Start"].value
    if "End" in args:
        end_date = args["End"].value

    conn = pymysql.connect(
        db='WeatherLog',
        user='WeatherLog',
        passwd='Weather!',
        host='localhost')
    cursor = conn.cursor()

    for table, formatter in gather:
        cursor.execute("""
            SELECT
                Logged, AVG(Value)
            FROM
                {0}
            WHERE
                Logged >= '{1}' AND Logged <= '{2}'
            GROUP BY
                ROUND(UNIX_TIMESTAMP(Logged)/({3} * 60))
            ORDER BY
                Logged DESC
            LIMIT
                1;""".format(table, start_date, end_date, minute_group))

        for row in cursor.fetchall():
            print "{2}\t{0}\t{1}".format(row[0], formatter(row[1]), table[:-3])

And then finally we have history.py, which returns the history for a single metric in tab-delimited format:

#!/usr/bin/python
from datetime import datetime, timedelta

import cgi
import pymysql


DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
DEFAULT_DAY_LIMIT = 7
DEFAULT_MINUTE_GROUPING = 3


if __name__ == '__main__':
    print "Content-type: text/html"
    print

    args = cgi.FieldStorage()
    start_date = (datetime.utcnow() - timedelta(days = DEFAULT_DAY_LIMIT)).strftime(DATE_FORMAT)
    end_date = datetime.utcnow().strftime(DATE_FORMAT)
    minute_group = DEFAULT_MINUTE_GROUPING
    table = "TemperatureLog"

    formatter = lambda c: c * 1.8 + 32

    if "LogType" in args:
        log_type = args["LogType"].value
        if log_type == "Humidity":
            table = "HumidityLog"
            formatter = lambda x: x
        elif log_type == "BmpTemp":
            table = "BmpTempLog"
            formatter = lambda c: c * 1.8 + 32
        elif log_type == "Pressure":
            table = "PressureLog"
            formatter = lambda p: p * 0.0002953
        elif log_type == "Altitude":
            table = "AltitudeLog"
            formatter = lambda m: m * 3.28084
        elif log_type == "SeaPressure":
            table = "SeaPressureLog"
            formatter = lambda p: p * 0.0002953

    if "Start" in args:
        start_date = args["Start"].value
    if "End" in args:
        end_date = args["End"].value

    conn = pymysql.connect(
        db='WeatherLog',
        user='WeatherLog',
        passwd='Weather!',
        host='localhost')
    cursor = conn.cursor()

    cursor.execute("""
        SELECT
            Logged, AVG(Value)
        FROM
            {0}
        WHERE
            Logged >= '{1}' AND Logged <= '{2}'
        GROUP BY
            ROUND(UNIX_TIMESTAMP(Logged)/({3} * 60));""".format(table, start_date, end_date, minute_group))

    didfirst = 0
    print "Logged\tValue"
    for row in cursor.fetchall():
        didfirst = 1
        print "{0}\t{1}".format(row[0], formatter(row[1]))

Any suggestions, advice and critiques are welcome. This is going to slowly get larger, and I'm going to be significantly increasing what it does so I want to get the structure and design setup correctly initially.