Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save TaylorBurnham/f24597d0c5cb03f635c9fc039418f789 to your computer and use it in GitHub Desktop.
Save TaylorBurnham/f24597d0c5cb03f635c9fc039418f789 to your computer and use it in GitHub Desktop.
Telegraf + InfluxDB + Airthings + Python

Telegraf + InfluxDB + Airthings + Python

This is a really quick and dirty way to pull data via BLE on my Raspbery Pi and toss it into InfluxDB. This only supports the WavePlus. If Airthings wants to send me free stuff I will extend it.

I quickly rewrote my script here to support this and eventually I will extend it.

https://github.com/TaylorBurnham/AirThings

Example Output

The default measurement is "environment" but you can call it whatever you want. Defaults to no output if it fails.

environment,device=WavePlusPlus,serial=86753095150,name=Hallway humidity=45.5,temperature=18.81,pressure=1022.1,radon_lt=18,radon_st=42,co2=871.0,voc=106.0

It tags each pull with the device make, serial, and alias/name from the airthings.json file. For the WavePlus it returns the following units:

  • Temperature (C)
  • Humidity (%rH)
  • Pressure (hPa)
  • Radon Short and Long Term (Bq/m3)
  • CO2 (PPM)
  • VOC (PPB)

If you're like me and always seen the pCi/L measurements of radon this is the approximate conversion below.

Unit Name Conversion
Bq/m3 Becquerels per Cubic Meter 37 Bq/m3 = 1 pCi/L
pCi/L Picocuries per Liter 1 pCi/L = 37 Bq/m3

If you want to understand all of the safe tolerances and more when it comes to Radon exposure I recommend taking a look at the excerpts on this NIH page for Evaluation of Guidelines for Exposures to Technologically Enhanced Naturally Occurring Radioactive Materials.

High Level Steps

  1. Install bluepy within a Python 3 virtual environment
  2. Install the script and update line #1 to point to that environment unless you installed it globally for some sick reason.
  3. Make the script executable. chmod +x /etc/telegraf/exec.d/airthings-wave.py
  4. Make the script owned by root unless you're a fool because of step #6 chown root:root /etc/telegraf/exec.d/airthings-wave.py
  5. Add the configuration file airthings.json with each of your devices to pull in the same directory.
  6. Add the telegraf file to your /etc/sudoers.d/ directory and be sure you followed step #4 or you're gonna have a bad day.
  7. Install the input plugin. Configure the timeout to 10 seconds per device.
  8. Restart Telegraf and make sure your metrics are showing up under the telegraf database.

I'll put some sample dashboards and other things in the repository I linked above once I've had it soak for a bit.

#!/etc/telegraf/exec.d/exec.denv/bin/python
import os
import sys
import json
import time
import struct
from datetime import datetime
from bluepy.btle import UUID, Peripheral, Scanner, DefaultDelegate
class WavePlusPlus():
def __init__(self, SerialNumber):
self.periph = None
self.curr_val_char = None
self.MacAddr = None
self.SN = SerialNumber
self.uuid = UUID("b42e2a68-ade7-11e4-89d3-123b93f75cba")
def connect(self):
if self.MacAddr is None:
scanner = Scanner().withDelegate(DefaultDelegate())
searchCount = 0
# Loop for 50 scans or until we find it.
while self.MacAddr is None:
devices = scanner.scan(0.1)
searchCount += 1
for dev in devices:
ManuData = dev.getValueText(255)
if ManuData:
SN = self.parseSerialNumber(ManuData)
if SN == self.SN:
self.MacAddr = dev.addr
if searchCount >= 50:
# Set to false if we hit 50.
# No breaks.
self.MacAddr = False
if self.MacAddr is None or not self.MacAddr:
sys.exit(1)
# Initialize Device
if self.periph is None:
self.periph = Peripheral(self.MacAddr)
if self.curr_val_char is None:
self.curr_val_char = self.periph.getCharacteristics(
uuid=self.uuid
)[0]
def disconnect(self):
if self.periph:
self.periph.disconnect()
self.periph = None
self.curr_val_char = None
def read(self):
if (self.curr_val_char is None):
sys.exit(1)
raw_data = self.curr_val_char.read()
sensor_data = self.parseSensors(raw_data)
return sensor_data
@staticmethod
def parseSerialNumber(ManuDataHexStr):
ManuData = bytearray.fromhex(ManuDataHexStr)
if (((ManuData[1] << 8) | ManuData[0]) == 0x0334):
SN = ManuData[2]
SN |= (ManuData[3] << 8)
SN |= (ManuData[4] << 16)
SN |= (ManuData[5] << 24)
else:
SN = "Unknown"
return SN
def parseSensors(self, raw_data):
raw_data = struct.unpack('BBBBHHHHHHHH', raw_data)
sensor_version = raw_data[0]
if sensor_version == 1:
# build sensors
sensor_data = {
"config": {
"version": sensor_version,
},
"atmospheric": {
"humidity": {
"value": raw_data[1] / 2.0,
"unit": "%rH"
},
"temperature": {
"value": raw_data[6] / 100.0,
"unit": "C"
},
"pressure": {
"value": raw_data[7] / 50.0,
"unit": "hPa"
}
},
"particle": {
"radon_lt": {
"value": self.conv2radon(raw_data[5]),
"unit": "Bq/m3"
},
"radon_st": {
"value": self.conv2radon(raw_data[4]),
"unit": "Bq/m3"
},
"co2": {
"value": raw_data[8] * 1.0,
"unit": "ppm"
},
"voc": {
"value": raw_data[9] * 1.0,
"unit": "ppb"
}
}
}
else:
sys.exit(1)
return sensor_data
@staticmethod
def conv2radon(radon_raw):
if 0 <= radon_raw <= 16383:
radon = radon_raw
else:
radon = -1 # bad read
return radon
basepath = (os.path.dirname(os.path.realpath(__file__)))
config = os.path.join(basepath, "airthings.json")
if os.path.isfile(config):
with open(config, 'r') as fh:
devices = json.load(fh)
else:
sys.exit(1)
for device in devices:
if device['model'] == 'WavePlusPlus':
sensors = None
waveplus = WavePlusPlus(device['serial'])
waveplus.connect()
sensors = waveplus.read()
waveplus.disconnect()
if sensors:
print("environment,device={DEVICE},serial={SERIAL},name={NAME} humidity={HUMIDITY},temperature={TEMP},pressure={PRESSURE},radon_lt={RADON_LT},radon_st={RADON_ST},co2={CO2},voc={VOC}".format(
DEVICE=device['model'], SERIAL=device['serial'], NAME=device['name'],
HUMIDITY=sensors['atmospheric']['humidity']['value'],
TEMP=sensors['atmospheric']['temperature']['value'],
PRESSURE=sensors['atmospheric']['pressure']['value'],
RADON_LT=sensors['particle']['radon_lt']['value'],
RADON_ST=sensors['particle']['radon_st']['value'],
CO2=sensors['particle']['co2']['value'],
VOC=sensors['particle']['voc']['value']))
[
{
"name": "Hallway",
"serial": 86753095150,
"model": "WavePlusPlus"
}
]
[[inputs.exec]]
commands = [
"sudo /etc/telegraf/exec.d/airthings-wave.py"
]
# I recommend against running this more than every 60 seconds.
interval = "60s"
# BLE can take a bit to return sometimes so I recommend
# setting your timeout 10 seconds per device.
timeout = "10s"
data_format = "influx"
# Place in /etc/sudoers.d/telegraf
# Permits telegraf to execute this one and only command as sudo. Be sure it's owned by root.
telegraf ALL=(ALL) NOPASSWD: /etc/telegraf/exec.d/airthings-wave.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment