Ok so months ago I started a new project where I wanted to know my immediate Air Quality Index so I bought the WINGONEER PM Sensor SDS011 High Precision PM2.5 Air Quality Detection Sensor Module Super Dust Sensors Digital Output from Amazon and then hooked it up to a Raspberry PI 3B+.
This will give you a REAL TIME notification of current AQI.
First you need to install this python script and have it start at run time..
#!/usr/bin/python -u
# coding=utf-8
# "DATASHEET": http://cl.ly/ekot
# https://gist.github.com/kadamski/92653913a53baf9dd1a8
from __future__ import print_function
import serial, struct, sys, time, json, subprocess
DEBUG = 0
CMD_MODE = 2
CMD_QUERY_DATA = 4
CMD_DEVICE_ID = 5
CMD_SLEEP = 6
CMD_FIRMWARE = 7
CMD_WORKING_PERIOD = 8
MODE_ACTIVE = 0
MODE_QUERY = 1
PERIOD_CONTINUOUS = 0
JSON_FILE = '/var/www/html/aqi.json'
MQTT_HOST = '192.168.0.36'
MQTT_TOPIC = '/weather/particulatematter'
ser = serial.Serial()
ser.port = "/dev/ttyUSB0"
ser.baudrate = 9600
ser.open()
ser.flushInput()
byte, data = 0, ""
def dump(d, prefix=''):
print(prefix + ' '.join(x.encode('hex') for x in d))
def construct_command(cmd, data=[]):
assert len(data) <= 12
data += [0,]*(12-len(data))
checksum = (sum(data)+cmd-2)%256
ret = "\xaa\xb4" + chr(cmd)
ret += ''.join(chr(x) for x in data)
ret += "\xff\xff" + chr(checksum) + "\xab"
if DEBUG:
dump(ret, '> ')
return ret
def process_data(d):
r = struct.unpack('<HHxxBB', d[2:])
pm25 = r[0]/10.0
pm10 = r[1]/10.0
checksum = sum(ord(v) for v in d[2:8])%256
return [pm25, pm10]
#print("PM 2.5: {} μg/m^3 PM 10: {} μg/m^3 CRC={}".format(pm25, pm10, "OK" if (checksum==r[2] and r[3]==0xab) else "NOK"))
def process_version(d):
r = struct.unpack('<BBBHBB', d[3:])
checksum = sum(ord(v) for v in d[2:8])%256
print("Y: {}, M: {}, D: {}, ID: {}, CRC={}".format(r[0], r[1], r[2], hex(r[3]), "OK" if (checksum==r[4] and r[5]==0xab) else "NOK"))
def read_response():
byte = 0
while byte != "\xaa":
byte = ser.read(size=1)
d = ser.read(size=9)
if DEBUG:
dump(d, '< ')
return byte + d
def cmd_set_mode(mode=MODE_QUERY):
ser.write(construct_command(CMD_MODE, [0x1, mode]))
read_response()
def cmd_query_data():
ser.write(construct_command(CMD_QUERY_DATA))
d = read_response()
values = []
if d[1] == "\xc0":
values = process_data(d)
return values
def cmd_set_sleep(sleep):
mode = 0 if sleep else 1
ser.write(construct_command(CMD_SLEEP, [0x1, mode]))
read_response()
def cmd_set_working_period(period):
ser.write(construct_command(CMD_WORKING_PERIOD, [0x1, period]))
read_response()
def cmd_firmware_ver():
ser.write(construct_command(CMD_FIRMWARE))
d = read_response()
process_version(d)
def cmd_set_id(id):
id_h = (id>>8) % 256
id_l = id % 256
ser.write(construct_command(CMD_DEVICE_ID, [0]*10+[id_l, id_h]))
read_response()
def pub_mqtt(jsonrow):
cmd = ['mosquitto_pub', '-h', MQTT_HOST, '-t', MQTT_TOPIC, '-s']
print('Publishing using:', cmd)
with subprocess.Popen(cmd, shell=False, bufsize=0, stdin=subprocess.PIPE).stdin as f:
json.dump(jsonrow, f)
if __name__ == "__main__":
cmd_set_sleep(0)
cmd_firmware_ver()
cmd_set_working_period(PERIOD_CONTINUOUS)
cmd_set_mode(MODE_QUERY);
while True:
cmd_set_sleep(0)
for t in range(15):
values = cmd_query_data();
if values is not None and len(values) == 2:
print("PM2.5: ", values[0], ", PM10: ", values[1])
time.sleep(2)
# open stored data
try:
with open(JSON_FILE) as json_data:
data = json.load(json_data)
except IOError as e:
data = []
# check if length is more than 100 and delete first element
if len(data) > 100:
data.pop(0)
# append new values
jsonrow = {'pm25': values[0], 'pm10': values[1], 'time': time.strftime("%d.%m.%Y %H:%M:%S")}
data.append(jsonrow)
# save it
with open(JSON_FILE, 'w') as outfile:
json.dump(data, outfile)
if MQTT_HOST != '':
pub_mqtt(jsonrow)
print("Going to sleep for 1 min...")
cmd_set_sleep(1)
time.sleep(60)
Then you can install my driver to pull the data....
/*
* Personal AQI driver
*/
import groovy.json.JsonSlurper
import java.util.GregorianCalendar
metadata {
definition (name: "AQI Monitor", namespace: "sgrayban",
author: "Scott Grayban",
importURL: "")
{
capability "Sensor"
capability "Initialize"
//attribute "switch","ENUM",["on","off"]
attribute "pm10", "number"
attribute "pm25", "number"
attribute "pm10Level", "string"
attribute "pm25Level", "string"
}
preferences {
input name: "MQTTBroker", type: "text", title: "MQTT Broker Address:",
required: true, displayDuringSetup: true
input name: "username", type: "text", title: "MQTT Username:",
description: "(blank if none)", required: false, displayDuringSetup: true
input name: "password", type: "password", title: "MQTT Password:",
description: "(blank if none)", required: false, displayDuringSetup: true
input name: "topicSub", type: "text", title: "Topic to Subscribe:",
description: "Example Topic (/weather/particulatematter). Please don't use a #",
required: true, displayDuringSetup: true
input name: "QOS", type: "text", title: "QOS Value:", required: false,
defaultValue: "1", displayDuringSetup: true
input("logEnable", "bool", title: "Enable logging", required: true, defaultValue: true)
}
}
def installed() {
log.info "installed..."
}
// Parse incoming device messages to generate events
def parse(String description) {
msg = interfaces.mqtt.parseMessage(description)
topic = msg.get('topic')
payload = msg.get('payload')
if (logEnable) log.info "${payload}"
def parser = new JsonSlurper()
if (topic == "/weather/particulatematter") {
def pr_vals = parser.parseText(payload)
state.pm10 = pr_vals['pm10']
state.pm25 = pr_vals['pm25']
//log.info = "${state.pm10}"
sendEvent(name: "pm10", value: state.pm10, displayed: true)
sendEvent(name: "pm25", value: state.pm25, displayed: true)
if (state.pm10 >= 0 && state.pm10 <= 54) {
sendEvent(name: "pm10Level", value: "<font color='green'>Good</font>", isStateChange: true);
} else
if (state.pm10 >= 55 && state.pm10 <= 345) {
sendEvent(name: "pm10Level", value: "<font color='yellow'>Moderate</font>", isStateChange: true);
} else
if (state.pm10 >= 355 && state.pm10 <= 424) {
sendEvent(name: "pm10Level", value: "<font color='red'>Unhealthy</font>", isStateChange: true);
} else
if (state.pm10 >= 425 && state.pm10 >= 9999) {
sendEvent(name: "pm10Level", value: "<font color='red'>Hazardous</font>", isStateChange: true);
}
if (state.pm25 >= 0 && state.pm25 <= 12.0) {
sendEvent(name: "pm25Level", value: "<font color='green'>Good</font>", isStateChange: true);
} else
if (state.pm25 >= 12.1 && state.pm25 <= 35.4) {
sendEvent(name: "pm25Level", value: "<font color='yellow'>Moderate</font>", isStateChange: true);
} else
if (state.pm25 >= 35.5 && state.pm25 <= 150.4) {
sendEvent(name: "pm25Level", value: "<font color='red'>Unhealthy</font>", isStateChange: true);
} else
if (state.pm25 >= 150.5 && state.pm25 <= 250.4) {
sendEvent(name: "pm25Level", value: "<font color='red'>Very Unhealthy</font>", isStateChange: true);
} else
if (state.pm25 >= 250.5 && state.pm25 >= 500.4) {
sendEvent(name: "pm25Level", value: "<font color='red'>Hazardous</font>", isStateChange: true);
}
/* PM25 Levels
0 to 12.0 - Good
12.1 to 35.4 - Moderate
35.5 to 150.4 - Unhealthy
150.5 to 250.4 - Very Unhealthy
250.5 to 500.4 - Hazardous
PM10 (in µg/m³)
Good 0-54
Moderate 55-154
Unhealthy for sensitive individuals 155-254
Unhealthy 255-354
Very unhealthy 355-424
Hazardous 425-504
Hazardous 505-604
Hazardous 605-9999
*/
}
}
def updated() {
if (logEnable) log.info "Updated..."
initialize()
}
def uninstalled() {
if (logEnable) log.info "Disconnecting from mqtt"
interfaces.mqtt.disconnect()
}
def initialize() {
if (logEnable) runIn(900,logsOff) // clears debugging after 900 secs
if (logEnable) log.info "Initalize..."
try {
def mqttInt = interfaces.mqtt
//open connection
mqttbroker = "tcp://" + settings?.MQTTBroker + ":1883"
mqttInt.connect(mqttbroker, "hubitat_${device}", settings?.username,settings?.password)
//give it a chance to start
pauseExecution(1000)
log.info "Connection established"
def topic = settings?.topicSub + "/#"
mqttInt.subscribe(topic)
if (logEnable) log.debug "Subscribed to: ${topic}"
} catch(e) {
if (logEnable) log.debug "Initialize error: ${e.message}"
}
}
def mqttClientStatus(String status) {
if (status.startsWith("Error")) {
def restart = false
if (! interfaces.mqtt.isConnected()) {
log.warn "mqtt isConnected false"
restart = true
} else if (status.contains("lost")) {
log.warn "mqtt Connection lost detected"
restart = true
} else {
log.warn "mqtt error: ${status}"
}
if (restart) {
def i = 0
while (i < 60) {
// wait for a minute for things to settle out, server to restart, etc...
pauseExecution(1000*60)
initialize()
if (interfaces.mqtt.isConnected()) {
log.warn "mqtt reconnect success!"
break
}
i = i + 1
}
}
} else {
if (logEnable) log.warn "mqtt OK: ${status}"
}
}
def logsOff(){
log.warn "Debug logging disabled."
device.updateSetting("logEnable",[value:"false",type:"bool"])
}
The driver states
My driver code is rough and I left all my comments in there.
There is NO COPYRIGHT on this and I expect no one else to pawn it off as theirs and put one in.