header

As RRDTool wasn’t a perfect tool for storing sensor data, I tried a newer datastore: InfluxDB. It is a database designed specifically for time-series data, such as my tempereture readings here.

For visualizing that data, I used Grafana, The open platform for beautiful analytics and monitoring.

The ansible scripts for this post can be found at Github, namely the playbook tempreader-influxdb.yml.

InfluxDB

Setting up InfluxDB is done in the ansible role influxdb. It will install the server from Influxdata repository, copy its configuration to place, start the server, add a database user with admin rights, create a database and open required ports in ufw firewall.

tempreader-influxdb.yml playbook, relevant parts:

roles:
  - { role: "influxdb", when: "'server' in group_names" }

roles/influxdb/tasks/main.yml:

- name: "add influxdb package repository key"
  apt_key:
    url: "https://repos.influxdata.com/influxdb.key"
    state: "present"

- name: "add influxdb package repository"
  apt_repository:
    repo: "deb https://repos.influxdata.com/debian jessie stable"
    state: "present"

- name: "install influxdb"
  apt:
    name: "influxdb"
    update_cache: "yes"
  notify: "start influxdb"

- name: "copy influxdb.conf"
  template:
    src: "influxdb.conf.j2"
    dest: "/etc/influxdb/influxdb.conf"
  notify: "restart influxdb"

- name: "ensure that influxdb is running before adding admin user"
  meta: "flush_handlers"

- name: "wait for influxdb to start"
  pause:
    seconds: 10

- name: "add influxdb admin user"
  uri:
    url: "http://localhost:{{ influxdb_port }}/query"
    method: "POST"
    body: "q=CREATE USER {{ influxdb_user }} WITH PASSWORD '{{ influxdb_pass }}' WITH ALL PRIVILEGES"
    user: "{{ influxdb_user }}"
    password: "{{ influxdb_pass }}"

- name: "create database"
  uri:
    url: "http://localhost:{{ influxdb_port }}/query"
    method: "POST"
    body: "q=CREATE DATABASE {{ influxdb_dbname }}"
    user: "{{ influxdb_user }}"
    password: "{{ influxdb_pass }}"

- name: "open firewall ports for influxdb"
  ufw:
    rule: "allow"
    port: "{{ item }}"
  with_items:
    - "{{ influxdb_admin_port }}"
    - "{{ influxdb_port }}"

Variables for that role are given in tempreader-influxdb.yml playbook, and eventually in an ansible vault file:

vars:
  influxdb_admin_port: "8083"
  influxdb_port: "8086"
  influxdb_host: "{{ secret.influxdb_host }}"
  influxdb_user: "{{ secret.influxdb_user }}"
  influxdb_pass: "{{ secret.influxdb_pass }}"
  influxdb_dbname: "{{ secret.influxdb_dbname }}"

I have it running on an external server, but you should be able to install it also on a Pi device. The only thing with Grafana is that if you use the cloud-version of Grafana, InfluxDB should be available for connections from the internet.

Readings to InfluxDB

Sending the temperature readings from Pi to database is done with a simple python script that is executed periodically with cron.

tempreader-influxdb.yml playbook:

roles:
  - { role: "onewire", when: "'tempreader' in group_names" }
  - { role: "tempsensor-config", when: "'tempreader' in group_names" }
  - { role: "tempreader-influxdb", when: "'tempreader' in group_names" }

Onewire role is discussed already in post Raspberry Pi and 1-Wire temperature sensor. It will setup the Raspberry Pi’s 1-Wire bus for DS18B20 sensors.

roles/tempsensor-config/tasks/main.yml will setup a JSON file that describes all the available temperature reading sensors so that their 1-Wire ids will have also more meaningful tags in the readings:

- name: "copy temperature sensors config file"
  become_user: "{{ ansible_user }}"
  template:
    src: "sensors.json.j2"
    dest: "{{ script_install_path }}/sensors.json"

roles/tempsensor-config/tasks/main.yml has Jinja2 magic to create a pretty JSON file from ansible data:

{
{% for sensor in sensors %}
  "{{ sensor.id }}": {
    "shortname": "{{ sensor.shortname }}",
    "name": "{{ sensor.name }}"
  }{% if not loop.last %},{% endif %}

{% endfor %}
}

Sensor configuration is defined in group_vars/all/vars.yml:

sensors:
  - id: "05168020fdff"
    shortname: "oloh-tvtaso"
    name: "Olohuone TV-taso"
  - id: "05168043d3ff"
    shortname: "mh-ulko"
    name: "Makuuhuone ulko"
  - id: "05168068aeff"
    shortname: "mh-sisa"
    name: "Makuuhuone sisä"

roles/tempreader-influxdb/tasks/main.yml sets up the temperature reading script: it installs python and required libraries, copies the script, sets up the cron and runs it for the first time.

- name: "install required software from apt"
  apt:
    name: "{{ item }}"
  with_items:
    - "python"
    - "python-pip"

- name: "install python libraries"
  pip:
    name: "{{ item }}"
  with_items:
    - "w1thermsensor"
    - "influxdb"

- name: "install temperature reading script"
  become_user: "{{ ansible_user }}"
  become: true
  template:
    src: "readtemp-influxdb.py.j2"
    dest: "{{ script_install_path }}readtemp-influxdb.py"

- name: "add temperature reading to cron"
  cron:
    name: "read all temperature sensors to influxdb"
    job: "/usr/bin/python {{ script_install_path }}readtemp-influxdb.py"
    minute: "*/1"
    user: "{{ ansible_user }}"

- name: "run temperature reading for the first time"
  become_user: "{{ ansible_user }}"
  become: true
  shell: "/usr/bin/python {{ script_install_path }}readtemp-influxdb.py"

Finally, the interesting part roles/tempreader-influxdb/templates/readtemp-influxdb.py.j2 isn’t after all that special. It will read temperature values with W1ThermSensor and send data to InfluxDB with InfluxDBClient.

Tags for each reading are used from the JSON configuration file sensors.json.

DS18B20 temperature reading range is -55–+125°C, but it will send +85°C on error. Thus, all values above +80°C are filtered out as they wouldn’t be sane anyways in this setup.

from w1thermsensor import W1ThermSensor
from influxdb import InfluxDBClient
import datetime
import platform
import json

dbclient = InfluxDBClient(
    host="{{ influxdb_host }}",
    username="{{ influxdb_user }}",
    password="{{ influxdb_pass }}",
    port={{ influxdb_port }},
    database="{{ influxdb_dbname }}"
)

with open('sensors.json') as f:
    sensor_tags = json.load(f)

measurements = []

for sensor in W1ThermSensor.get_available_sensors():
    temp = sensor.get_temperature()
    if temp < 80: # 85 is an errorcode
        tags = sensor_tags.get(sensor.id, {})
        tags["sensor_id"] = sensor.id
        tags["host"] = platform.node()
        measurements.append({
            "measurement": "temperature",
            "time": datetime.datetime.utcnow().isoformat() + 'Z',
            "tags": tags,
            "fields": {
                "value": temp
            }
        })

print json.dumps(measurements, indent=4, sort_keys=True)
if measurements: dbclient.write_points(measurements)

InfluxDB can be queried manually with an SQL-like syntax:

rh@sokrates:~$ influx -database 'temperature' -username '...' -password '...'
Connected to http://localhost:8086 version 1.4.2
InfluxDB shell version: 1.4.2

> select * from temperature order by time desc limit 10;
name: temperature
time                host           name             sensor_id    shortname   value
----                ----           ----             ---------    ---------   -----
1532181008754363136 rpi-makuuhuone Makuuhuone sisä  05168068aeff mh-sisa     26.062
1532181007874356992 rpi-makuuhuone Makuuhuone ulko  05168043d3ff mh-ulko     23.875
1532181004805038080 rpi-olohuone   Olohuone TV-taso 05168020fdff oloh-tvtaso 28.437
1532180948674241024 rpi-makuuhuone Makuuhuone sisä  05168068aeff mh-sisa     25.812
1532180947794324992 rpi-makuuhuone Makuuhuone ulko  05168043d3ff mh-ulko     23.687
1532180944884818944 rpi-olohuone   Olohuone TV-taso 05168020fdff oloh-tvtaso 28.5
1532180888674245120 rpi-makuuhuone Makuuhuone sisä  05168068aeff mh-sisa     25.75
1532180887794349824 rpi-makuuhuone Makuuhuone ulko  05168043d3ff mh-ulko     23.625
1532180885044857088 rpi-olohuone   Olohuone TV-taso 05168020fdff oloh-tvtaso 28.562
1532180829634234880 rpi-makuuhuone Makuuhuone sisä  05168068aeff mh-sisa     25.562

Grafana

For data visualization, Grafana was easy to set up. I used the hosted grafana as it was free for single user. Grafana can also be installed to your own server.

So there are no ansible scripts for this part. After creating a hosted grafana instance, you can follow the good instructions Using InfluxDB in Grafana. From the main points listed in Grafana’s Home Dashboard, important ones are adding a datasource and creating a dashboard.

Datasource configuration is straightforward, here are the configuration options and respective variables in tempreader-influxdb.yml:

For me, the dashboard configuration wasn’t intuitive at first. After clicking new dashboard and then “Graph”, you’ll end up with a blank panel and no hints on how to go forward. The trick is to click “Panel title” and then “Edit”. Then configure the query as follows:

grafana - new dashboard

For the query under “Metrics” tab, set up like this:

grafana - query

Here the GROUP BY clause will separate the data into one graph per sensor. tag(name) is the best tag for grouping as it will show up in the legend also. fill(previous) will connect the data points in the graph, and selecting previous will make “current” to show the latest data point in the legend.

In “Legend” tab, select:

grafana - query

And you’ll have a beautiful graph of your temperature readings!

grafana - graph

You can leave questions and comments to Reddit.