Build an Air Quality Sensor with a MQ135 Sensor to detect smoke and other harmful gas for Alexa

In this section, we will walk you through creating an air quality sensor using ESP32 or ESP8266. You will then be able to view the air quality via Amazon Alexa and receive a push notification via the Sinric Pro app when the air quality drops below a certain threshold level.

Prerequisites :

  1. ESP32, ESP8266 x 1.
  2. MQ135 x 1.
  3. Jumper Wires.

Sinric Pro MQ135 air quality

Quick introduction to MQ135

The MQ135 is a semiconductor gas sensor that is commonly used in air quality control equipment. It is sensitive to a wide range of gases, including ammonia, sulfide, benzene, and smoke. The sensor has two outputs: a digital output that goes high when the gas concentration exceeds a certain threshold, and an analog output that provides a voltage proportional to the gas concentration.

The MQ135 sensor requires a power supply of 5V and consumes about 150mA of current. The digital output can be connected to a ESP32, ESP8266 or Pico W to measure the gas concentration.

How to calibrate the MQ135 sensor

The MQ135 does not output ppm directly. Its resistance (Rs) changes with the gas concentration, and the ppm value is calculated from the ratio between Rs and R0, the sensor’s resistance in clean air. R0 is different for every sensor, so each one has to be calibrated.

1. Burn-in (once): Put your sensor outside where there is enough fresh air (ideally at 20°C & 33% humidity according to the datasheet) and leave it powered on for 12-24 hours to burn it in and stabilize.

2. Warm-up (every power on): the sensor has a heater. Readings are too high for the first few minutes after power on, so wait until they settle before using them.

3. Measure R0 in clean air: with the sensor still in fresh air, let the TroykaMQ library used in this tutorial calculate R0 and print it:

#include <TroykaMQ.h>

#define PIN_MQ135 34 // ESP32. Use A0 on ESP8266

MQ135 mq135(PIN_MQ135);

void setup() {
  Serial.begin(115200);
  mq135.calibrate();               // run this in clean air
  Serial.print("Ro = ");
  Serial.println(mq135.getRo());
}

4. Reuse the value: write down the printed R0 and pass it to calibrate(). The sensor then does not need fresh air every time it restarts:

mq135.calibrate(YOUR_RO_VALUE);

Temperature and humidity change the reading, so calibrate in conditions close to where the sensor will be used. The MQ135 is good for detecting that air quality has changed, but it is not a precision instrument.

MQ135 reading stuck high (around 3800 to 4095)?

On an ESP32, analogRead() returns 0 to 4095. A reading that stays at or near the top of that range usually has one of these causes:

Specification sheet: SNS-MQ135

Wiring

Sinric Pro ESP32 ESP8266 MQ135 air quality sensor wiring diagram

MCU A0 Pin VCC Pin
ESP32 34 (Analog ADC1_CH6) 5V
ESP8266 A0 5V/VIN
Pico W GP26 (ADC0) 5V

Note: on ESP32, ADC2 (GPIO04, GPIO02, GPIO15) is unstable when Wi-Fi is being used.

Let’s verify that motion sensor is wired correctly and working.

#include <TroykaMQ.h> // https://github.com/amperka/TroykaMQ

#if defined(ESP8266)
  #define SENSOR_PIN        A0
#elif defined(ESP32) 
  #define SENSOR_PIN        34
#endif

MQ135 mq135(SENSOR_PIN);

void setup() {
   Serial.begin(9600);
   // before calibrating the sensor, warm it up for 60 seconds calibrate the sensor in clean air
   mq135.calibrate();
   // if you know the resistance of the sensor in clean air
   // you can specify it manually, eg: 160 mq135.calibrate(160);   
   Serial.print("Ro = ");
   Serial.println(mq135.getRo());
}

void loop() {
   // display gas values in ppm
   Serial.print(mq135.readCO2());
   Serial.println("ppm");
   delay(1000);
}

View this code on GitHub Gist

Arduino IDE Serial Monitor will show the motion detections like this:

Sinric Pro mq135 test reading

Detecting poor Air Quality

#include <TroykaMQ.h> // https://github.com/amperka/TroykaMQ

#if defined(ESP8266)
  #define SENSOR_PIN        A0
#elif defined(ESP32) 
  #define SENSOR_PIN        34
#endif

MQ135 mq135(SENSOR_PIN);

const int AQI_THRESHOLD_SEVERE = 700;
const int AQI_THRESHOLD_VERY_POOR = 600;
const int AQI_THRESHOLD_POOR = 500;
const int AQI_THRESHOLD_MODERATE = 400;

int ppm = 0;

void setup() {
   Serial.begin(9600);
   // before calibrating the sensor, warm it up for 60 seconds calibrate the sensor in clean air
   mq135.calibrate();
   // if you know the resistance of the sensor in clean air
   // you can specify it manually, eg: 160 mq135.calibrate(160);   
   Serial.print("Ro = ");
   Serial.println(mq135.getRo());
}

void loop() {
   ppm = mq135.readCO2();
   
   if (ppm > AQI_THRESHOLD_SEVERE) {
    Serial.print("Air quality is SEVERE!");
   } else if (ppm > AQI_THRESHOLD_VERY_POOR) {
    Serial.print("Air quality is VERY POOR!");
   } else if (ppm > AQI_THRESHOLD_POOR) {
    Serial.print("Air quality is VERY POOR!");
   } else if (ppm > AQI_THRESHOLD_MODERATE) {
    Serial.print("Air quality is MODERATE");
   } else {
    Serial.print("Air quality is GOOD");
   }

   delay(1000);
}

View this code on GitHub Gist

Step 1 : Connect to Sinric Pro

Step 1.1 : Creating a custom device type for Air Quality Sensor.

Sinric Pro does not have a built-in device type for Air Quality Sensor with PPM so we are going to create a custom device type for Water Sensor using Device Template feature to achieve.

  1. PPM Value

  2. Air Quality Is Severe, Very Poor, Poor, Moderate or Good.

Note: You can use the device template import feature mentioned below to skip creating the full template.

Sinric Pro air quality ensor device template

Here we must select the features of our Air Quality Sensor. We want to know whether Air Quality is Severe, Very Poor, Poor, Moderate or Good. So let’s drag a Range, a Mode and Push Notification capability.

Sinric Pro custom device type for MQ135 air quality sensor with range and mode capabilities

Click on Configure button and setup the two capabilities like below.

Sinric Pro moisture sensor template mode and range settings

Click on Save to save.

Sinric Pro moisture sensor template mode and range settings

Click on Save to save the template.

Now you can see the template we just created.

Import an existing template?

If you are feeling lazy setup all the Modes and Range values, you can use the import feature.

Sinric Pro capacitive soil moisture sensor import template

Paste this Template:

{
  "name": "Air Quality Sensor",
  "description": "Air Quality Sensor",
  "deviceTypeId": "5ff0b112994fd31b7d5e8065",
  "capabilities": [
    {
      "id": "5ff0b41b994fd31b7d5e8961",
      "mode": {
        "instanceId": "modeInstance1",
        "locale": "en-US",
        "modeName": "Air Quality",
        "nonControllable": true,
        "modeValues": [
          "SEVERE",
          "VERY POOR",
          "POOR",
          "MODERATE",
          "GOOD"
        ]
      }
    },
    {
      "id": "5ff0b45f994fd31b7d5e89c2",
      "range": {
        "instanceId": "rangeInstance1",
        "locale": "en-US",
        "rangeName": "ppm",
        "minimumValue": 1,
        "maximumValue": 1024,
        "precision": 1,
        "unitOfMeasure": "600e7986a7a09c3f3230f8fb",
        "presets": [],
        "nonControllable": true
      }
    },
    {
      "id": "62a707bd9e2f39090569e2d3"
    }
  ]
}

View this code on GitHub Gist

Sinric Pro create device alexa

Sinric Pro create device alexa

Sinric Pro copy device id

Sinric Pro copy device id

Step 2 : Connect to Sinric Pro

Step 2.1 Install Sinric Pro Library

Sinric Pro install SinricPro library

Step 2.2 Install TroykaMQ

TroykaMQ is not available in Arduino Library Manager. So you must download it from https://github.com/amperka/TroykaMQ and copy it over to C:\Users\<your username>\Documents\Arduino\libraries

2.2 Complete Code

AirQualitySensor.h

#ifndef _AIRQUALITYSENSOR_H_
#define _AIRQUALITYSENSOR_H_

#include <SinricProDevice.h>
#include <Capabilities/ModeController.h>
#include <Capabilities/RangeController.h>
#include <Capabilities/PushNotification.h>

class AirQualitySensor 
: public SinricProDevice
, public ModeController<AirQualitySensor>
, public RangeController<AirQualitySensor>
, public PushNotification<AirQualitySensor> {
  friend class ModeController<AirQualitySensor>;
  friend class RangeController<AirQualitySensor>;
  friend class PushNotification<AirQualitySensor>;
public:
  AirQualitySensor(const String &deviceId) : SinricProDevice(deviceId, "AirQualitySensor") {};
};

#endif

sinricpro-air-quality-sensor.ino


// Uncomment the following line to enable serial debug output
//#define ENABLE_DEBUG

#ifdef ENABLE_DEBUG
  #define DEBUG_ESP_PORT Serial
  #define NODEBUG_WEBSOCKETS
  #define NDEBUG
#endif

#include <Arduino.h>
#ifdef ESP8266
  #include <ESP8266WiFi.h>
#endif
#ifdef ESP32
  #include <WiFi.h>
#endif

#include <TroykaMQ.h> // https://github.com/amperka/TroykaMQ
#include <SinricPro.h>
#include "AirQualitySensor.h"

#define WIFI_SSID         ""   // Your WiFI SSID name 
#define WIFI_PASS         ""   // Your WiFi Password.

#define APP_KEY           ""   // Should look like "de0bxxxx-1x3x-4x3x-ax2x-5dabxxxxxxxx" (Get it from Portal -> Secrets)
#define APP_SECRET        ""   // Should look like "5f36xxxx-x3x7-4x3x-xexe-e86724a9xxxx-4c4axxxx-3x3x-x5xe-x9x3-333d65xxxxxx" (Get it from Portal -> Secrets)
#define DEVICE_ID         ""   // Should look like "5dc1564130xxxxxxxxxxxxxx" (Get it from Portal -> Devices)

#define BAUD_RATE         115200              // Change baudrate to your need (used for serial monitor)
#define EVENT_WAIT_TIME   60000               // send event every 60 seconds


#define BAUD_RATE         115200
#define EVENT_WAIT_TIME   60000               // send event every 60 seconds

#if defined(ESP8266)
  #define SENSOR_PIN        A0
#elif defined(ESP32) 
  #define SENSOR_PIN        34
#endif

MQ135 mq135(SENSOR_PIN);

const int AQI_THRESHOLD_SEVERE = 700;
const int AQI_THRESHOLD_VERY_POOR = 600;
const int AQI_THRESHOLD_POOR = 500;
const int AQI_THRESHOLD_MODERATE = 400;

int ppm = 0;
int lastPPM = 0;
String lastAirQualityLevelStr = "";

AirQualitySensor &airQualitySensor = SinricPro[DEVICE_ID];
 
// ModeController
void updateMode(String mode) {
  airQualitySensor.sendModeEvent("modeInstance1", mode, "PHYSICAL_INTERACTION");
}

// RangeController
void updateRangeValue(int value) {
  airQualitySensor.sendRangeValueEvent("rangeInstance1", value);
}

// PushNotificationController
void sendPushNotification(String notification) {
  airQualitySensor.sendPushNotification(notification);
}

/********* 
 * Setup *
 *********/

void setupAirQualitySensor() {
   // before calibrating the sensor, warm it up for 60 seconds calibrate the sensor in clean air
   mq135.calibrate();
   // if you know the resistance of the sensor in clean air
   // you can specify it manually, eg: 160 mq135.calibrate(160);   
   Serial.print("Ro = ");
   Serial.println(mq135.getRo());
}

void setupSinricPro() {
  SinricPro.onConnected([]{ Serial.printf("[SinricPro]: Connected\r\n"); });
  SinricPro.onDisconnected([]{ Serial.printf("[SinricPro]: Disconnected\r\n"); });
  SinricPro.begin(APP_KEY, APP_SECRET);
};

void setupWiFi() {
  #if defined(ESP8266)
    WiFi.setSleepMode(WIFI_NONE_SLEEP); 
    WiFi.setAutoReconnect(true);
  #elif defined(ESP32)
    WiFi.setSleep(false); 
    WiFi.setAutoReconnect(true);
  #endif

  WiFi.begin(SSID, PASS);
  Serial.printf("[WiFi]: Connecting to %s", SSID);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.printf(".");
    delay(250);
  }
  Serial.printf("connected\r\n");
}

void setup() {
  Serial.begin(BAUD_RATE);
  setupAirQualitySensor();
  setupWiFi();
  setupSinricPro();
}

/********
 * Loop *
 ********/

 void handleAirQualitySensor() {
  if (SinricPro.isConnected() == false) {
    return; 
  }

  static unsigned long last_millis;
  unsigned long        current_millis = millis();
  if (last_millis && current_millis - last_millis < EVENT_WAIT_TIME) return;
  last_millis = current_millis;
    
  ppm = mq135.readCO2();

  Serial.printf("Present ppm: %d\r\n", ppm); 
  
  if (ppm == lastPPM) {
    Serial.printf("PPM did not changed. do nothing...!\r\n");
    return; 
  }

  /* determine air quality level */
  String airQualityLevelStr = "";

  if (ppm > AQI_THRESHOLD_SEVERE) {
    airQualityLevelStr = "SEVERE";
   } else if (ppm > AQI_THRESHOLD_VERY_POOR) {
    airQualityLevelStr = "VERY POOR";
   } else if (ppm > AQI_THRESHOLD_POOR) {
    airQualityLevelStr = "POOR";
   } else if (ppm > AQI_THRESHOLD_MODERATE) {
    airQualityLevelStr = "MODERATE";
   } else {
    airQualityLevelStr = "GOOD";
   }

  /* Update air quality level label if changed */
  if(lastAirQualityLevelStr != airQualityLevelStr) {
    lastAirQualityLevelStr = airQualityLevelStr;
    updateMode(lastAirQualityLevelStr);
  }
  
  delay(500);

  /* update PPM label */
  updateRangeValue(ppm);

  /* send a push notification when air quality level is bad! */
  if (ppm > AQI_THRESHOLD_SEVERE) {
    sendPushNotification("Air quality is SEVERE!");
  } else if (ppm > AQI_THRESHOLD_VERY_POOR) {
    sendPushNotification("Air quality is VERY POOR!");
  } else if (ppm > AQI_THRESHOLD_POOR) {
    sendPushNotification("Air quality is VERY POOR!");
  }

  lastPPM = ppm;  // save to compare next time.
}

void loop() {
  handleAirQualitySensor();
  SinricPro.handle();
}

View this code on GitHub Gist

Now you should be able to see the air quality status and water level via Alexa, Sinric Pro App

Alexa, What’s the air quality (mode name) in air quality sensor(device name)

Alexa, What’s the ppm (range name) in air quality sensor(device name)

Sinric Pro MQ135 air quality sensor in the Alexa app

Sinric Pro MQ135 air quality sensor in the Portal

Step 3 : Voice Announcements!

✅ How it works:

1️⃣ Add a Switch in SinricPro (your secret trigger!)

2️⃣ Create an Alexa Routine that speaks your message when a “Air quality is bad!” when Switch turned on

3️⃣ Send a power state event to turn ON that switch from your code

void triggerVoiceAnnouncement() {
    SinricProSwitch& mySwitch = SinricPro[SWITCH_ID];
    mySwitch.sendPowerStateEvent(true);
}

Troubleshooting

  1. Google Home or SmartThings are not Supported.

  2. Please refer to our Troubleshooting page for more details.

This document is open source. See a typo? Please create an issue