Tuesday, February 11, 2020

CO2 air quality monitor with Telegram notification

Introduction


CO2 Air Quality Monitoring System with Telegram notification


Purpose


The aim of this project is to receive Telegram notification when CO2 air quality is higher than certain level from Wemos. It uses MH-Z19B sensor to measure CO2 air quality and visualizes the result with Google Chart API. CO2 measurement data is presented in gauge type. We can check current status via web browser using WiFi connection.

Features


This project is based on our previous projects,

IoT Laboratory: CO2 air quality monitor with Line notification V2
IoT Laboratory: CO2 air quality monitor with Line notification

It replaces Line notification with Telegram notification.  A user will be notified by Telegram when the level of CO2 hits a certain level which is specified by the user in the source code. The entire process for sending Telegram notification is like below. Wemos sends HTTP GET request to Telegram server, then, the message will be delivered to our Telegram messenger.


For web server, we use ESPAsyncWebServer library and it is available at github. Additionally, we need to install EPS8266 Sketch Data Upload to upload web server related files to Wemos. For example, in this project, we use HTML, Javacript, and CSS files under data directory. In other words, we need to upload sketch and web related files separately.

Prerequisites


- Arduino IDE
ESP8266 package for Arduino IDE
EPS8266 Sketch Data Upload
ESPAsyncTCP Library
ESPAsyncWebServer Library
- Telegram Bot token and chat id

Requirements


Hardware
-Wemos D1 mini : US$1.77 on Aliexpress
-MH-Z19B CO2 sensor : US$17.90 on Aliexpress

Instructions


Step 1. Setup hardware

MH-Z19B sensor has many pins, but, we only use TX, RX, VCC, and GND pins. Connect TX of MH-Z19B to D5 of Wemos, RX to D6, VCC to 5V, and GND to G. Finally, connect micro usb to Wemos for uploading firmware, and check serial monitor and serial plotter in Android IDE to make sure the sensor works correctly.



Step 2. Create Telegram account and bot

To be able to receive Telegram notification, we need two things; bot token and chat id. Bot token is generated automatically when we create a new bot. Then, send a request on web browser to Telegram. After that, Telegram sends a response with chat id in it. This process looks a little bit complicated at first, but, I found an easy and useful tutorial on Youtube. It would be extremely helpful if you are not familiar with Telegram.

Step 3. Upload sketch to Wemos D1 mini

This step is to upload sketch to Wemos as usual. In the following sketch, following values need to be modified with your own.

WIFI_SSID  : Name of WiFi router
WIFI_PASS  Password of WiFi router
- TG_TOKEN   : Token of Telegram bot
TG_CHAT_ID Chat id that the bot will be sending message to

In this example sketch, it send Line notification only when CO2 density hit 1000 or higher. And before sending again it waits 10 minutes. These values can be changed by user.

notifyLevel    : CO2 threshold for Line notification
- notifyInterval : Waiting time (specify in milliseconds)


/**
* CO2 air quality monitoring system using MH_Z19B sensor with WIFI, WEBSERVER, Telegram NOTIFY
*
* Hardware : Wemos D1 mini, MH_Z19B
* Software : Arduino IDE, EPS8266 Sketch Data Upload
* Library : ESPAsyncTCP, ESPAsyncWebServer
*
* JSON format
* {'status':'starting'|'preheating'|'measuring', 'co2':0~5000, 'uptime': 120}
*
* Cautious!
* After cold start, it takes about 70 seconds for MH_Z19B to preheat itself. During this period,
* the reading will be somewhere between 410 and 430. But, after preheating period, it starts showing
* actual CO2 density. Therefore, it is recommended to read sensor after 70 seconds at least.
*
* MH-Z19B Manual:
* https://www.winsen-sensor.com/d/files/infrared-gas-sensor/mh-z19b-co2-ver1_0.pdf
* Telegram API Reference :
* https://core.telegram.org/bots/api
*
* February 2020. Brian Kim
*/
#include <SoftwareSerial.h>
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <FS.h>
#include <WiFiClientSecureAxTLS.h>
/**
* WiFi credentials
*/
#define WIFI_SSID "your_ssid"
#define WIFI_PASS "your_password"
/**
* Telegram credentials
*/
#define TG_TOKEN "your_telegram_token"
#define TG_CHAT_ID "your_telegram_chat_id"
/**
* MH_Z19B sensor pin map and command packet
*/
#define MH_Z19B_TX D5 // GPIO12
#define MH_Z19B_RX D6 // GPIO14
#define MH_Z19B_CMD 0xFF
#define MH_Z19B_RES 0x86
#define MH_Z19B_DATA_LENGTH 9
byte _cmd[MH_Z19B_DATA_LENGTH] = {MH_Z19B_CMD,0x01,MH_Z19B_RES,0x00,0x00,0x00,0x00,0x00,0x79};
/**
* Wemos serial RX - TX MH_Z19B
* TX - RX
*/
SoftwareSerial _serial(MH_Z19B_TX, MH_Z19B_RX); // RX, TX
AsyncWebServer server(80);
int _co2, _temp;
String status = "starting";
unsigned long uptime;
unsigned long previousMillis = 0;
const long notifyInterval = 600000; // Waiting time(milliseconds)
const int notifyLevel = 1000; // CO2 threshold for notification
bool waitFlag = false;
/**
* Preheating timing
*/
unsigned long startupMillis = 0;
/**
* Measuring interval
*/
unsigned long measuringMillis = 0;
const long measuringInterval = 5000; // Waiting time(milliseconds)
void connectWifi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
/**
* Notify Telegram
* https://api.telegram.org/bot__TOKEN__/sendmessage?chat_id=__CHAT_ID__&text=__MESSAGE__
*/
void notifyTG(String msg) {
WiFiClientSecure client;
client.setInsecure();
if (!client.connect("api.telegram.org", 443)) {
Serial.println ("ERROR Connection failed");
return;
}
String req = "GET /bot" + String(TG_TOKEN)
+ "/sendmessage?chat_id=" + String(TG_CHAT_ID)
+ "&text=" + msg
+ " HTTP/1.1\r\n";
req += "Host: api.telegram.org\r\n";
req += "Cache-Control: no-cache\r\n";
req += "User-Agent: ESP8266\r\n";
req += "Connection: close\r\n";
req += "\r\n";
client.print(req);
Serial.print(req);
while (client.connected()) {
String line = client.readStringUntil('\n');
if (line == "\r") {
break;
}
Serial.println(line);
}
}
void notifyLine(String msg) {
WiFiClientSecure client;
client.setInsecure();
if (!client.connect("notify-api.line.me", 443)) {
Serial.println ("ERROR Connection failed");
return;
}
String req = "POST /api/notify HTTP/1.1\r\n";
req += "Host: notify-api.line.me\r\n";
req += "Authorization: Bearer " + String(LINE_TOKEN) + "\r\n";
req += "Cache-Control: no-cache\r\n";
req += "User-Agent: ESP8266\r\n";
req += "Connection: close\r\n";
req += "Content-Type: application/x-www-form-urlencoded\r\n";
req += "Content-Length: " + String(String("message=" + msg).length()) + "\r\n";
req += "\r\n";
req += "message=" + msg;
client.print(req);
Serial.print(req);
while (client.connected()) {
String line = client.readStringUntil('\n');
if (line == "\r") {
// delay(20000);
break;
}
Serial.println(line);
}
}
String readSensor() {
byte checksum = 0;
unsigned char res[MH_Z19B_DATA_LENGTH] = {0,};
int avail = 0;
int count = 0;
_serial.flush();
_serial.write(_cmd, MH_Z19B_DATA_LENGTH);
delay(400);
while(_serial.available() &&
_serial.read() != MH_Z19B_CMD &&
_serial.peek() != MH_Z19B_RES ) {
}
if ( _serial.available() >= MH_Z19B_DATA_LENGTH-1 ) {
res[0] = MH_Z19B_CMD;
for (int j=1;j<MH_Z19B_DATA_LENGTH; j++) {
res[j] = _serial.read();
// Serial.printf("%2X ",res[j]);
if ( j < MH_Z19B_DATA_LENGTH-1 )
checksum += res[j];
}
// Serial.println();
checksum = 0xFF - checksum;
checksum++;
if (res[8] == checksum) {
// uptime = (millis()-startupMillis)/1000;
_co2 = makeWord(res[2],res[3]);
if ( _co2 <= 430 && uptime < 65 ) {
status = "preheating";
} else {
status = "measuring";
}
_temp = res[4]-40;
Serial.printf("CO2:%4d TEMP:%2d ELAPSED:%d Seconds STATUS:%s\n", _co2,
_temp,
uptime,
status.c_str());
} else {
Serial.printf("Error: CMD:%X RES:%X Checksum:%X:%X\n", res[0], res[1], checksum, res[8]);
}
} else {
Serial.println("Not ready");
}
return String(_co2);
}
void setup()
{
startupMillis = millis();
Serial.begin(115200); // For debugging
_serial.begin(9600); // For communicating with MH_Z19B sensor
Serial.printf("\nCO2 air quality monitoring system using MH-Z19B sensor with WIFI, WEBSERVER, LINE NOTIFY\n");
Serial.printf("CO2 notify level:%d Waiting time:%d seconds\n",notifyLevel, notifyInterval/1000);
// Initialize filesystem
if(!SPIFFS.begin()){
Serial.println("ERROR SPIFFS failed");
return;
}
connectWifi(); // Connect to WiFi
String msg="C02 sensor http://"+WiFi.localIP().toString();
// notifyLine(msg);
notifyTG(msg);
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
// request->send(SPIFFS, "/index.html", String(), false, templateHandler);
request->send(SPIFFS, "/index.html", "text/html");
});
server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/style.css", "text/css");
});
server.on("/script.js", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/script.js", "text/javascript");
});
server.on("/line_logo.png", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/line_logo.png", "image/png");
});
/**
* Send measured data back to client
* Data format : CO2
* Example : 500
*/
server.on("/updatesensorreading", HTTP_GET, [](AsyncWebServerRequest *request){
String ret;
ret = "{\"status\":\"" + status + "\",";
if ( status != "measuring" ) {
ret += "\"readystatus\":" + String(uptime*100/70) +",";
}
ret += "\"co2\":" + String(_co2) + ","
+"\"temp\":" + String(_temp) + ","
+"\"uptime\":" + String(uptime) + "}";
Serial.println(ret);
request->send_P(200, "application/json", ret.c_str());
});
server.begin();
}
void loop()
{
unsigned long currentMillis = 0;
unsigned long currentMeasuringMillis = 0;
if (_co2 >= notifyLevel && !waitFlag) {
String msg = "CO2 level is ";
Serial.printf("CO2: %d\n",_co2);
msg += String(_co2);
// notifyLine(msg);
notifyTG(msg);
waitFlag = true;
previousMillis = millis();
}
if (waitFlag) {
currentMillis = millis();
if ( currentMillis - previousMillis >= notifyInterval ) {
waitFlag = false;
previousMillis = currentMillis;
}
}
currentMeasuringMillis = millis();
if ( currentMeasuringMillis - measuringMillis > measuringInterval ) {
readSensor();
measuringMillis = currentMeasuringMillis;
}
uptime = (millis()-startupMillis)/1000;
}
view raw gistfile1.txt hosted with ❤ by GitHub
Step 4. Upload data to Wemos D1 mini

This step is to upload web server related files (HTML, Javascript, CSS) to Wemos. These files are located in directory named data. Click "ESP8266 Sketch Data Upload" under Tools menu in Arduino IDE to upload these files to Wemos. Once it shows the measurement data, it will refresh every 3 seconds automatically.

HTML file

On web interface, the size of gauge is defined in width, height of options variable. Just change these values to customize chart size. And if you want to modify refresh rate, change the value of 30000 in setInterval function to other value.

<!-- Air quality monitoring system using MH_Z19B sensor with WIFI, WEBSERVER
Hardware : Wemos D1 mini, MH_Z19B
Software : Arduino IDE, EPS8266 Sketch Data Upload
Library : ESPAsyncTCP, ESPAsyncWebServer -->
<!DOCTYPE html>
<html>
<head>
<title>Air quality monitor</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="mobile-web-app-capable" content="yes">
<link rel="stylesheet" type="text/css" href="style.css">
<script src="https://www.gstatic.com/charts/loader.js"></script>
<script src="script.js"></script>
</head>
<body onload="startClock()">
<h1>CO2 Sensor</h1>
<div id="clock_div"></div>
<div>
<div id="status_div">Status:</div>
<progress id="progress" value=0 max=100></progress>
</div>
<div class="gauge" id="chart_div"></div>
<div id="line_chart_div"></div>
</body>
</html>
view raw gistfile1.txt hosted with ❤ by GitHub
CSS file

/* Air quality monitoring system using MH_Z19B sensor with WIFI, WEBSERVER
Hardware : Wemos D1 mini, MH_Z19B
Software : Arduino IDE, EPS8266 Sketch Data Upload
Library : ESPAsyncTCP, ESPAsyncWebServer */
html {
font-family: monospace;
}
h1 {
font-size:32px;
}
body {
text-align: center;
/* background-image: url("line_logo.png");
background-repeat: no-repeat; */
}
div {
align-content: center;
margin: 0 auto;
}
.gauge {
display: grid;
margin: 0 auto;
}
#clock_div {
/* font-size:27px; */
}
progress {
-webkit-appearance: none;
height: 2px;
vertical-align: super;
}
progress::-webkit-progress-bar {
background-color: #FFFFFF;
}
progress::-webkit-progress-value {
background-color: #90EE90;
}
view raw gistfile1.txt hosted with ❤ by GitHub

Javascript file

google.charts.load('current', {'packages':['gauge','corechart']});
// Display clock above air quality gauge
function startClock() {
var now = new Date();
var hour = now.getHours();
var minute = now.getMinutes();
var second = now.getSeconds();
minute = minute < 10 ? "0" + minute : minute;
second = second < 10 ? "0" + second : second;
document.getElementById("clock_div").innerHTML =
hour + ":" + minute + ":" + second;
var tmp = setTimeout(startClock, 500);
}
google.charts.setOnLoadCallback(drawChart);
function drawChart() {
var data = google.visualization.arrayToDataTable([
['Label', 'Value'],
['CO2', 0]
]);
var options = {
redFrom: 2000, redTo: 5000,
yellowFrom:1000, yellowTo: 1999,
greenFrom:0, greenTo: 999,
max: 5000,
minorTicks: 10,
majorTicks: ["0","1000","2000","3000","4000","5000"]
};
var chart = new google.visualization.Gauge(document.getElementById('chart_div'));
chart.draw(data, options);
var data2 = google.visualization.arrayToDataTable([
['Time', 'CO2'],
['0', 0]]);
var options2 = {
title: 'CO2 density',
hAxis: {textPosition: 'none'},
vAxis: {title:'PPM', viewWindow: {min:0,max:3000}},
legend: { position: 'bottom' }
};
var chart2 = new google.visualization.LineChart(document.getElementById('line_chart_div'));
chart2.draw(data2, options2);
var n=1;
setInterval(function ( ) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
var obj = JSON.parse(this.responseText);
var pms = obj.co2;
console.log(obj);
if ( obj.status != "measuring" ) {
document.getElementById("progress").style.visibility = "visible";
document.getElementById("progress").setAttribute("value", obj.readystatus);
} else {
document.getElementById("progress").style.visibility = "hidden";
}
document.getElementById('status_div').innerHTML = "Status: "+obj.status;
data.setValue(0, 1, pms);
chart.draw(data, options);
// data2.addRow([n.toString(), parseInt(pms)]);
data2.addRow([Date().toString(), parseInt(pms)]);
chart2.draw(data2, options2);
n++;
}
};
xhttp.timeout = 2000;
xhttp.ontimeout = function() {
document.getElementById('status_div').innerHTML = "Status: disconnected";
}
xhttp.open("GET", "/updatesensorreading", true);
xhttp.send();
}, 3000 );
}
view raw script.js hosted with ❤ by GitHub

Results


After uploading firmware, Wemos restarts itself automatically. Once Wemos D1 mini has restarted, serial monitor shows a welcome message, CO2 threshold, and waiting period. When CO2 is higher than threshold it will send Telegram notification to the user as below.


In this project, Wemos sends two types of message. The first type is for informing IP address of itself.  And the second type is for notification with measured CO2 density.


We can use web browser to connect to the device, then it will show current time and visualized information, which is based on Google Chart API. It will refresh itself every 3 seconds automatically.


Cautious!


In this project, Wemos uses softwareserial library to communicate with MH-Z19B sensor. However,  this library will be interfered by other library which uses timer or interrupt internally. Originally, I plant to use Adafruit Neopixel library in this project to lit some of LED strip, but, it interfered the communication of softwareserial library. So, I decided not to use LED strip for this project. One possible work around would be to ESP32 board which has more than one hardware serial.

References


- MH-Z19B Manual PDF: mh-z19b-co2-ver1_0.pdf
MHZ19 - RevSpace
Source codes at github

No comments:

Post a Comment