[PROJECT] AQI using the AQI Sensor SDS011

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
image

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.

2 Likes

Changing the display info you can do this with your dashboard.

image

You can see this working in real-time at http://50.214.37.90/ -- you can also see all the history of the sensor as well.

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.