eCO2-Telegram-Bot

There is not always a necessity for highly accurate and expensive measuring devices to determine indoor air quality. A rough reading and limit monitoring can also be very helpful if you simply want to know when it is appropriate to ventilate an intensively used room. Such a device becomes even more useful when its simple local display is supplemented by an alarm output to the smartphones that are constantly carried around today.

A highly integrated sensor device, a modern microcontroller and a little BASIC programming make it possible to use this eCO2 monitor and its various display modes to indicate poor air quality with very little technical effort in four ways :

  • local NeoPixel LED(s) , as air quality traffic light
  • web interface for local devices with a web browser
  • manual query via Telegram messenger
  • Telegram alert message sent to a dedicated Telegram user
eCO2-Telegram-Bot with M5Stack ATOM-matrix and CCS811

.

The Hardware: The simple circuit is based on an eCO2-Sensor CCS811, an ESP32-SoC-module. The air quality is represented by one ore more NeoPixel LEDs. A single button is connected to one input pin. The sensor and the Soc communicate via a two wire I2C-bus. The NeoPixel uses only one data wire at GPIO27, no matter if a single LED dot or a matrix is addressed.

Almost any ESP32-module with exposed I2C-pins will do the job. But I recommend a M5Stack “ATOM matrix” or “ATOM lite”. This devices contain in a small protective housing an ESP32-Pico-D4 with an antenna , a build-in NeoPixel-matrix or single NeoPixel dot and a built-in push button. Thus only two hardware components get placed on a simple grid board. If the CCS811 and the ATOM are plugged to corresponding sockets and pin headers respectively, the necessary few wires are very easy to connect. The downside of the grid board gets isolated by adhesive tape.

The circuit is powered from a USB power supply (5V@max 500mA) via the USB-C connector, the HY2.0 Grove port or the downside sockets.

The eCO2-Sensor CCS811:  
The CCS811 sensor cannot measure the CO2 content directly! It calculates the equivalent CO2 content (eCO2) by measuring the tVOC (total Volatile Organic Compound), where the main source of VOCs is the air exhaled by humans.

This cheap type of sensor searches a baseline of relative  “good” air by determining the best condition in a longer interval and  then assuming the sensor being  in fresh unpolluted air at that time.
But: The sensor does not store this value by itself.

Additionally the sensor sensitivity can change over the time and under different environmental conditions like temperature and humidity. 
Thus for  giving reliable eCO2 readings the sensor needs:

  • a one-time burn-in-time  of more than 48 hours
  • a minimum run-in of about 20minutes after each  cold start.

The datasheet contains more detailed information about the gas sensor.

The Software:
The Program was developed with and runs within  ANNEX32 –  a BASIC interpreter for ESP32. After burning into the flash RAM of the ESP32 device via an installation program using a USB serial port, the interpreter and its development environment run completely in the ESP32. Only a Chrome or Firefox browser is needed to load, edit, test and (automatically) execute the BASIC script. The minimum required Annex version is V1.435, as this includes CCS811 and Telegram messenger support. The online help file of Annex32 is a helpful introduction to this BASIC interpreter.

The main tasks of the BASIC code:

  • The CCS811 is initialized and asked for the eCO2 level once per second.
  • The air eCO2 status gets  categorized  as  GREEN, AMBER or RED.
  • The build-in NeoPixel-Matrix or a single NeoPixel LED locally indicates the category by its color.
  • A Webinterface displays the eCO2-Value  and the category via a browser on the (W)LAN.
  • The category and eCO2 value can be queried manually via Telegram messenger, as the program works with a Telegram bot and fetches incoming user commands from the Telegram server
  • A Telegram warning message is automatically sent to the last Telegram chat_id when the air quality is in the RED range. 
  • The baseline of the air quality can be stored  manually by pressing the front button of the ATOM-module or by an incoming Telegram command

The Telegram-Bot routine regularly requests the telegram servers for incoming commands. It responds to  this commands:

  • /e   returns eCO2 value  and category [GREEN|YELLOW|RED]
  • /s   stores the baseline in /baseline.txt
  • /r    restores the baseline from /baseline.txt
  • /i    returns the local IP-settings of the module
  • [any other character]  same as  /e

How to get your own TELEGRAM_Token:
To use the Telegram functions in the BASIC program you first have to create your own Telegram-Bot by following the instructions of BotFather at https://t.me/BotFather in your Telegram APP. This will provide your personal Telegram token and a bot name. These can be inserted in the BASIC-code lines to set the appropriate variables.

The use of ANNEX32-BASIC has certainly at least the advantage, even for inexperienced programmers, to be well readable and also adaptable to own needs. For a better understanding of the functions, the code is abundantly provided with comments.
This script can be pasted into the web-based editor of ANNEX32 and stored into the ESP32 module as an autorun file in e.g. /default.bas .

DON’T FORGET TO INSERT YOUR OWN TELEGRAM TOKEN

'#######################################################################
' eCO2-Telegram-Bot
' ##################
'# Shows AIR QUALITY via the estimated concentration of carbon dioxide
'# calculated from known TVOC concentration. This is based on the
'# assumption that the VOC produced by human lungs is proportional to their exhaled CO2.
'
'# USED HARDWARE:
'  - ESP32-device; best: M55tack "ATOM lite" with one built-in NeoPixel
'    or "ATOM matrix" with buit-in 5*5-NeoPixels
'  - CCS811 eCO2-TVOC-Sensor at I2C-pins of the  ESP32
'  - switch-button (e.g. the built-in front button of the M5Stack ATOM)
'
'# SOFTWARE: BASIC-interpreter  Annex32  V1.48.22  @ http://cicciocb.com/forum
'
'# OUTPUT of eCO2-ppm  via ...
'- NEOPIXEL_LED with at least 1 pixel to indicate the range [GREEN|AMBER|RED] of eCO2 ppm
'- WebInterface to display eCO2-ppm value and air condition [GREEN|AMBER|RED]
'- TELEGRAM-BOT  message-on-demand with eCO2-ppm value and air condition [GREEN|AMBER|RED]
'- TELEGRAM-BOT  alert message, if condition reaches RED eCO2-range for more than x measures values

'# Saves the  BASELINE of the CCS811 as "GOOD-AIR-CONDITION" in file
'   /baseline.txt if switch-button is pressed for more than 3 seconds

'# Sends a TELEGRAM Alert message to current CHAT_ID if RED condition is detected

'# Respons to TELEGRAM-BOT commands (with or without leading /) :
' /a => RED-alerts will from now on be sent to the user who has send this command (until next reboot)
' /i => return local IP-Settings  (useful to identify the local dynamic  IP-Address of the module)
' /s => store the current baseline representing the "clean-air-condition"
'      in file  /baseline.txt
' /r => restore the baseline from file  /baseline.txt
' /e or [any other character] returns the  eCO2-ppm-value
'    and the air condition [GREEN|amber|RED]

'#######################################################################
' Peter Neufeld 2023/01; DB9JG@me.com
Version$           = "V2.6"

'------SETTINGS-------------
RESTORE_BASELINE   = 1 '1 => after a run-in time: restores the  baseline from data file if available
SAVE_BASELINE      = 0 '1 => after a run-in time: save current air condition as the "clean air" baseline
SILENT             = 0 '1 => suppress  all messages   0 => show some status-messages via  wlog
LOCATION$          = "ROOM #1"  'a description of the sensor location etc

TELEGRAM_activated = 1 '1 => activates the Telegram-BOT (TOKEN AND NAME NEEDED!!!!)
TELEGRAM_TOKEN$    = "xxxxxxxxxx:AAHQ3qTIj248QCU0ao_GcetggK3l_nhANj4"
TELEGRAM_BOTNAME$  = "yyyyyyyyyyyyyyyy"
T_CHAT_ID_Alert$   = "123456789" 'My own private chat ID as default destination for alerts
T_CHAT_ID$         = T_CHAT_ID_Alert$ 'if <> "" then alerts will immediately go there

TELEGRAM_MSG_Threshold      = 5 'Message if bad-air-condition for too long
TELEGRAM_MAX_FAILED_COUNTER = 0
TELEGRAM_MAX_FAILED         = 10 'reboot if too many failed communication
TELEGRAM_is_still_running   = 0

LL_GREEN    = 400   'lower limit for the green LED;  usually ~400
LL_AMBER   = 1000  'lower limit for the AMBER LED; usually ~1000
LL_RED      = 1800  'lower limit for the red LED;    usually ~1800
HYST        = 0.07  'Hysteresis factor to  stabilize  the  RED alert status
LL_RED_HYST = 0     'temporay hysteresis correction
'---------------------------
eCO2        = 0
eTVOC       = 0
CONDITION$  = "GREEN"
COND_COL$   = CONDITION$
CONDITION_OLD$ = CONDITION$
T$          = time$
STR_eCO2$   = ""

gosub        SETUP_PERIPHERAL_HARDWARE
IF RESTORE_BASELINE = 1  gosub CCS811_RESTORE_BL_FROM_FILE
gosub        MAKE_WEBPAGE
onhtmlreload MAKE_WEBPAGE
onHtmlChange MAKE_WEBPAGE
IF TELEGRAM_activated    gosub TELEGRAM_INIT

timer0 1000, MAIN   'the main program is called  once per second 
wait
end '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

'####################################################################################
'####################################################################################
MAIN:
'-------------
gosub READ_eCO2
gosub SHOW_eCO2

T$        = time$
STR_eCO2$ = str$(eCO2)
if CONDITION$ <> CONDITION_OLD$ then gosub MAKE_WEBPAGE 'change the webpage

IF condition$ = "RED"  and T_CHAT_ID$ <>"" then
  RED_COUNT = RED_COUNT +1
else   'reset counter if good condition returns before reaching threshold
  RED_COUNT = 0
endif
'===Send a telegram alert message to latest CHAT_ID if too many RED conditions-----
IF RED_COUNT = TELEGRAM_MSG_Threshold then gosub TELEGRAM_send_alert
'===-------------------------------------------------------------------------------

'++++restore the latest baseline regularly after a run-in time of 10 minutes
count=(count +1) mod (10*60)
'if count = (10*60)-1 then gosub CCS811_SAVE_BASELINE_TO_FILE
if count = (10*60)-1  gosub CCS811_RESTORE_BL_FROM_FILE
'++++--------------------------------------------------------
'---SAVE baseline if frontbutton pressed for more then 3s
IF pin(FRONT_BUTTON) = PRESSED then
  PRESSED_COUNT = (PRESSED_COUNT + 1) mod 4
else
  PRESSED_COUNT = 0
ENDIF
IF PRESSED_COUNT = 3  gosub CCS811_SAVE_BASELINE_TO_FILE
'---

return

'####################################################################################
MAKE_WEBPAGE:
'-------------
'create the autorefreshing html page with dynamic display values and matching colors 
A$ = ""
A$ = A$ + "<H1>eCO2 " + LOCATION$
A$ = A$ + "</H1>"
A$ = A$ + "Time :" + textbox$(T$) + "<br>"
A$ = A$ + "eCO2: " + textbox$(STR_eCO2$) + "<br>"
A$ = A$ + |<span style="color:| + COND_COL$ + |">|
A$ = A$ + "<H1>Condition: "+ CONDITION$+ "</H1>"
A$ = A$ + "</span>"
cls
autorefresh 1000
html A$
return

'####################################################################################
SHOW_eCO2:
'---------
x=5-x 'toggle the LED-brigthness +0 or +5 just to show some activity
CONDITION_OLD$=CONDITION$
select case eCO2
  case 0 to  LL_AMBER                    'GREEN
    R=0 : G=x+20  : B=0
    CONDITION$ = "GREEN"
    COND_COL$  = CONDITION$
    LL_RED_HYST = 0
  case LL_AMBER to (LL_RED - LL_RED_HYST)'AMBER
    R=x+15 : G=x+10 : B=0
    CONDITION$ = "AMBER"
    COND_COL$  = "yellow"
    LL_RED_HYST = 0
  CASE (LL_RED - LL_RED_HYST)to 99999    'RED
    R=x+20 : G=0 : B=0
    CONDITION$ = "RED"
    COND_COL$  = CONDITION$
    LL_RED_HYST = (LL_RED * HYST)  'set a hysteresis to stabilize  the condition
End select
neo.strip 0,NEO_END,R,G,B    'show the condition at NeoPixel-stripe
wlog "eCO2 = "; eCO2, "  Condition: "; CONDITION$
return

'####################################################################################
READ_eCO2:
'---------
if ccs811.avail = 1 then
  a     = ccs811.read
  eCO2  = CCS811.CO2
  eTVOC = CCS811.TVOC
end if
return

'####################################################################################
SETUP_PERIPHERAL_HARDWARE:
'-------------------------
'I2C for CCS811
i2c.setup 21, 22  '  sda_pin: 21, scl_pin: 22

'built-in front-button to store the  current air condition as  clean air condition
FRONT_BUTTON  = 39 'pin39 for build-in button of M5Stack ATOM xxxx
PRESSED       = 0  'the built-in switch pulls down at ATOM devices
PRESSED_COUNT = 0
pin.mode FRONT_BUTTON, input

'initialize the NEOPIXELs
NEO_PIN = 27     'NeoPixel data-pin for  M5stack "ATOM xxxx" devices
NEO_NUM = 1     ' Number of Neopixels for a M5stack "ATOM lite"
if bas.device = 103 then    '103 is a M5Stack "ATOM matrix"
  NEO_PIN = 27
  NEO_NUM = 25              'Number of Neopixels
endif
NEO_END = NEO_NUM - 1

R=20 : G=20 : B=20  'initial colors for neopixel(s)
neo.setup NEO_PIN, NEO_NUM
neo.strip 0,NEO_END,R,G,B

'CCS811 eCO2-Sensor
if ccs811.Setup(&h5a) <> 0 then
  wlog ccs811.Setup(&h5a)
  print "CCS811 not found. Program stopped "
  wlog  "CCS811 not found. Program stopped "
  end   '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
endif
wlog ccs811.setdrivemode(1)  'update the sensor every second
return

'####################################################################################
CCS811_SAVE_BASELINE_TO_FILE:
'----------------------------
neo.strip 0,NEO_END,0,0,150   'set Neopixels color to blue to indicate reaction
BASELINE$ = str$(CCS811.GETBASELINE)
File.save "/BASELINE.txt", BASELINE$
wlog "Saved baseline ";BASELINE$ ; " to file /BASELINE.txt"
neo.strip 0,NEO_END,R,G,B     'Back to the former colors
return

'####################################################################################
CCS811_RESTORE_BL_FROM_FILE:
'---------------------------
BASELINE$  = "39103"  'default value if no file
if file.exists("/BASELINE.txt") then
  BASELINE$ = File.read$("/BASELINE.txt")
  if val(BASELINE$) >0 then
    wlog "CCS811.SETBASELINE returned: "; CCS811.SETBASELINE(val(baseline$))
    wlog "Restored baseline ";BASELINE$ ; " from file /BASELINE1.txt"
  endif
endif
return

'####################################################################################
TELEGRAM_INIT:
'-------------
WLOG "TELEGRAM_INIT"
telegram.settoken TELEGRAM_TOKEN$
telegram.setwait  10
telegram.setmode  0
onwgetasync       TELEGRAM_asynco
'send a start message to my own account
if T_CHAT_ID$ <> "" then
  tt$="eCO2-Sensor at  "+location$ +" is running now at local IP-Address"+ word$(IP$,1)
  WLOG telegram.sendmessage$(val(T_CHAT_ID$),tt$ )
endif
'Get the update each 10 seconds, to limit internet traffic
timer1 10000, TELEGRAM_getMessage
return

'####################################################################################
TELEGRAM_getMessage:
'-------------------
WLOG  time$;": TELEGRAM_getMessage:"
telegram.GetUpdatesAsync
return

'####################################################################################
TELEGRAM_send_alert:
'---------------
'send an ALERT message to latest seen telegram chat-id
'ATTENTION! No RED-alert-message, if there was no previous request from a client.
'You can provide a fixed chat_id to overcome this

if TELEGRAM_is_still_running then return  'to avoid a second call while still running
TELEGRAM_is_still_running = 1
onwgetasync   off   '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

If T_CHAT_ID$ <> "" then
  tt$ = "RED-ALERT condition  for " + LOCATION$ +" at "+ time$ + ": eCO2 = " + str_eCO2$  + "  Condition: " + CONDITION$
  neo.strip 0,NEO_END,30,30,30   'white Neopixels to  indicate a TELEGRAM transmission
  WLOG telegram.sendmessage$(val(T_CHAT_ID$),tt$ )
  WLOG tt$
  neo.strip 0,NEO_END,R,G,B      'back to former condition
  TELEGRAM_is_still_running = 0
  onwgetasync        TELEGRAM_asynco
endif
return

'####################################################################################
TELEGRAM_asynco:
'---------------
'Receive the messages and respond according to included command-string

if TELEGRAM_is_still_running then return
TELEGRAM_is_still_running = 1
onwgetasync   off   '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
r$ = WGETRESULT$
WLOG "The TELEGRAM_BOT-Service  returns at  "; Time$; ": "; r$
if instr(lcase$(r$)," failed") then
  TELEGRAM_MAX_FAILED_COUNTER = TELEGRAM_MAX_FAILED_COUNTER + 1
  WLOG "The TELEGRAM_BOT-Service failed; REBOOT countdown initialized : ", str$(TELEGRAM_MAX_FAILED - TELEGRAM_MAX_FAILED_COUNTER)
  if TELEGRAM_MAX_FAILED_COUNTER = TELEGRAM_MAX_FAILED then
    WLOG "REBOOT NOW..."
    reboot
  endif
endif
if instr(r$,|"ok"|) then TELEGRAM_MAX_FAILED_COUNTER = 0
c$ = json$(r$, "chat.id")   'get the chat_id
if c$ <>"not found" then T_CHAT_ID$=c$
tt$ = LOCATION$ +" at "+ time$ + ": eCO2 = " + str_eCO2$  + "  Condition: " + CONDITION$
text$ = json$(r$, "text")
if (text$ <> "not found") then
  text$=replace$(text$,"/","") 'delete /
  select case left$(lcase$(text$),1)
    case "i" : tt$=" Local IP-Address is " + word$(IP$,1)
    case "r"
      gosub CCS811_RESTORE_BL_FROM_FILE
      tt$="Restored the BASELINE from file"
    case "s"
      gosub CCS811_SAVE_BASELINE_TO_FILE
      tt$="Saved the BASELINE to file"
    case "a"
      T_CHAT_ID_Alert$ = T_CHAT_ID$
      tt$= "OK! From now on  all RED-alerts will go to this Telegram-user"
  end select
  neo.strip 0,NEO_END,30,30,30   'white Neopixels to  indicate a TELEGRAM transmission
  WLOG telegram.sendmessage$(val(T_CHAT_ID$),tt$ )
  neo.strip 0,NEO_END,R,G,B      'back to former condition
end if
TELEGRAM_is_still_running = 0
onwgetasync        TELEGRAM_asynco
return

'####################################################################################
Advertisement
%d bloggers like this: