diff options
| -rw-r--r-- | README.md | 27 | ||||
| -rwxr-xr-x | generate_rrd_db.sh | 98 | ||||
| -rw-r--r-- | mqtt_to_rrd.py | 160 | ||||
| -rw-r--r-- | settings.cfg.example | 25 | 
4 files changed, 310 insertions, 0 deletions
| diff --git a/README.md b/README.md new file mode 100644 index 0000000..11997c4 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +Contains two scripts: +    - Bash script generating rrd database and generating graphs from it +    - Python script readding from MQTT and updating rrd database +    - Settings for the Python script + +DEPENDENCIES: +    - configparser +    - paho +    - rrdtool +    kernel support ( amongst others (: ): +        - CONFIG_USB_VIDEO_CLASS + +Steps to get going: +    - Generate rrd file with ./generate_rrd_db.sh -c -i <rrd-file> +    - Set up MQTT broker. +    - Set up Raspberry Pi pico (Pico project in Micropython section) +    - Fill in settings.cfg with your data +    - Set up certificates (Check out Pico project for Bash script generating them) +    - Run Python script. +    - Set up cron job regenerating graphs with frequency to your liking. + +(So that I don't forget:) +What is still missing: +    - Python script should run as a daemon. +    - Python script could generate more graphs based on averages from rrd database showing long term trends. +    - Graph generation could be done with rrdcgi (not sure it is needed though) +    - Documentation is lacking detailed step by step setup guide. diff --git a/generate_rrd_db.sh b/generate_rrd_db.sh new file mode 100755 index 0000000..e81b64d --- /dev/null +++ b/generate_rrd_db.sh @@ -0,0 +1,98 @@ +#!/bin/bash + +RRD_DATABASE="" +RRD_GRAPH=w1_graph.png +# Base interval in seconds with which data will be fed into the database: +STEP=15 +# One wire sensor address: +ROM=00-055300000000  # TODO: update me with a real thing! +# Seconds in a year: 31'536'000 +GRAPH=false +CREATE=false + +function Help() { +    # Display help +    echo "Command generating graph from rrd database" +    echo +    echo "Syntax: generate_rrd [-h|-g|-c|-o|-i]" +    echo "options:" +    echo "-h     Print this help" +    echo "-g     Create graph" +    echo "-c     Create database" +    echo "-i     Input rrd database file" +    echo "-o     Output image, defaults to 'w1_graph.png'" +    exit 0 +} + + +# Create rrd database if not exist, +# TODO: generate more graphs based on averages +# Keep once in 5 minutes average, min and max for a month, +# Keep once an hour for a year: +function rrd_create() { +    if test -f "$RRD_DATABASE"; then +        echo "Database already exists, exiting." +        exit 1 +    else +      rrdcreate "$RRD_DATABASE" --step="$STEP" \ +      DS:temp:GAUGE:45:U:U \ +      RRA:AVERAGE:0.5:1:108000 \ +      RRA:AVERAGE:0.5:20:5400 \ +      RRA:MIN:0.5:20:5400 \ +      RRA:MAX:0.5:20:5400 \ +      RRA:AVERAGE:0.5:240:131400 \ +      RRA:MAX:0.5:240:131400 \ +      RRA:AVERAGE:0.5:240:131400 +    fi +} + +# Generate graph image: +rrd_graph() { +    echo "Creating graph $RRD_GRAPH" +    rrdtool graph "$RRD_GRAPH" --start -86400 --end now \ +      --vertical-label "Temperature C" \ +      --upper-limit 40 \ +      --lower-limit -30 \ +      -w 640 -h 480 \ +      DEF:temperature="$RRD_DATABASE":temp:AVERAGE \ +      LINE1:temperature#00FF00:"temperature C" +} + +while getopts hgci:o: flag; do +    case "${flag}" in +        h) Help # Display help +            exit;; +        i) # Input rrd file +            RRD_DATABASE=${OPTARG};; +        o) # Output image  +            RRD_GRAPH=${OPTARG};; +        c) # Create rrd database +            echo "Creating database ..." +            CREATE=true;; +        g) # Create graph +            echo "Creating graph ..." +            GRAPH=true;; +        \?) # Invalid option +            echo "Invalid option" +            Help;; +    esac +done + +if [ -z "$RRD_DATABASE" ] +then +    echo "Please specify rrd database." +    Help +fi + +if [[ $CREATE == false ]] && [[ $GRAPH == false ]];then +    echo "Specify either database creation (-c) or graph generation (-g) option" +    Help +fi + +if $CREATE; then +    rrd_create +fi + +if $GRAPH; then +    rrd_graph +fi diff --git a/mqtt_to_rrd.py b/mqtt_to_rrd.py new file mode 100644 index 0000000..70818eb --- /dev/null +++ b/mqtt_to_rrd.py @@ -0,0 +1,160 @@ +""" +Script reading from mqtt broker and writing to rrd database. +""" +######################################################### +#                       IMPORTS                         # +######################################################### + +import os +import ssl +from configparser import ConfigParser +from time import time +import rrdtool +from paho.mqtt.client import Client as MQTTClient + +######################################################### +#                       SETTINGS                        # +######################################################### + +CONFIG_FILE = 'settings.cfg' +USER = os.getlogin() +RRD_DATABASE = f"/home/{USER}/w1_database.rrd" + +######################################################### +#                           CODE                        # +######################################################### + + +SSL_SET = None +CERT_DIR = None +CA_CERT = None +CERT_FILE = None +KEY_FILE = None +ONE_WIRE_UPPER = None +ONE_WIRE_LOWER = None +HOSTNAME = None +PORT = None +KEEPALIVE = None +DATA_LENGTH_LIMIT = None +TOPIC = None +TIMEFORMAT = None +Y_LIMIT = None +X_LIMIT = None +INTERVAL = None + + +def get_settings(config_file): +    """ +    Get settings from configparser. +    """ +    cfg = ConfigParser(interpolation=None) +    cfg.read(config_file) + +    global SSL_SET +    global CERT_DIR +    global CA_CERT +    global CERT_FILE +    global KEY_FILE +    global ONE_WIRE_UPPER +    global ONE_WIRE_LOWER +    global HOSTNAME +    global PORT +    global KEEPALIVE +    global DATA_LENGTH_LIMIT +    global TOPIC +    global TIMEFORMAT +    global Y_LIMIT +    global X_LIMIT +    global INTERVAL +    # SSL certificates: +    SSL_SET = cfg.getboolean('Certificates', 'ssl') +    CERT_DIR = cfg.get('Certificates', 'certificate_directory') +    CA_CERT_PATH = os.path.join( +            CERT_DIR, +            cfg.get('Certificates', 'ca_crt') +    ) +    CERT_FILE_PATH = os.path.join( +            CERT_DIR, +            cfg.get('Certificates', 'cert_file'), +    ) +    KEY_FILE_PATH = os.path.join( +            CERT_DIR, +            cfg.get('Certificates', 'key_file') +    ) +    CA_CERT = CA_CERT_PATH if CA_CERT_PATH else None +    CERT_FILE = CERT_FILE_PATH if CERT_FILE_PATH else None +    KEY_FILE = KEY_FILE_PATH if KEY_FILE_PATH else None + +    # One wire mapping: +    ONE_WIRE_UPPER = cfg.get('Sensor_mapping', 'upper') +    ONE_WIRE_LOWER = cfg.get('Sensor_mapping', 'lower') + +    # MQTT host: +    HOSTNAME = cfg.get('Mqtt', 'hostname') +    PORT = cfg.getint('Mqtt', 'port') +    KEEPALIVE = cfg.getint('Mqtt', 'keepalive') + +    DATA_LENGTH_LIMIT = cfg.get('Main', 'data_length_limit') +    TOPIC = cfg.get('Main', 'topic') +    TIMEFORMAT = cfg.get('Main', 'timeformat') + +    # Plot settings: +    Y_LIMIT = ( +        cfg.getint('Main', 'y_min'), +        cfg.getint('Main', 'y_max') +    ) +    X_LIMIT = cfg.getint('Main', 'x_limit') +    INTERVAL = cfg.getint('Main', 'interval') + + +class MyMQTTClient(MQTTClient): +    """ +    MQTT client class on_message writing to rrd database. +    """ +    def __init__(self, *args, **kwargs): +        super().__init__(*args, **kwargs) +        self.data_limit = DATA_LENGTH_LIMIT +        if SSL_SET: +            self.tls_set( +                ca_certs=CA_CERT, +                certfile=CERT_FILE, +                keyfile=KEY_FILE, +                cert_reqs=ssl.CERT_REQUIRED, +            ) +        self.connect(host=HOSTNAME, port=PORT, keepalive=KEEPALIVE) +        self.bare_topic = TOPIC.strip("/#") +        self.full_topic = f'{self.bare_topic}/{ONE_WIRE_UPPER}' +        self.subscribe(self.full_topic) +        self.on_message = self.onmessage +        self.on_disconnect = self.ondisconnect + +    def ondisconnect(self, client): +        print("Disconnected from MQTT") +        client.loop_stop() + +    def on_connect(self, client, userdata, flags, rc): +        print("Connected to MQTT, returncode: ", str(rc)) +        # Subscribing in on_connect() means that if we lose the connection and +        # reconnect then subscriptions will be renewed. +        client.subscribe("$SYS/#") + +    def onmessage(self, client, userdata, msg): +        """ +        On message update rrd database. +        """ +        temperature = msg.payload.decode("utf-8") +        if msg.topic == self.full_topic: +            now = time()  # TODO: pass timestamp in the message body + +            rrdtool.update(RRD_DATABASE, '%s:%s' % (now, temperature)) + + +def main(): +    """Main loop""" +    get_settings(CONFIG_FILE) +    client = MyMQTTClient() +    client.loop_forever() + + +if __name__ == '__main__': +    main() diff --git a/settings.cfg.example b/settings.cfg.example new file mode 100644 index 0000000..0cdbc45 --- /dev/null +++ b/settings.cfg.example @@ -0,0 +1,25 @@ +[Main] +data_length_limit = 10 +timeformat = %Y-%m-%d %H:%M:%S +topic = temperature +y_max = 51 +y_min = -20 +x_limit = 2000 +interval = 5000 + +[Certificates] +ssl = True +certificate_directory = +ca_crt = +cert_file = +key_file = + +[Sensor_mapping] +upper = +lower = + +[Mqtt] +hostname = +port = 8883 +keepalive = 60 +topic = temperature | 
