4632

eCO2 monitor that indicates poor air quality via a TELEGRAM message, a local display and a web interface

This eCO2 monitor indicates poor air quality via a
  • local NeoPixel LED-Strip, via a
  • Web interface and via a
  • TELEGRAM message produced by a built-in telegram bot

 Hardware
Required are an eCO2-Sensor CCS811 and an ESP32 SoC-module - preferably an M5Stack "ATOM-matrix" or "ATOM lite" with a build-in NeoPixel-strip. The Hardware components get placed on a simple grid board or a breadboard  and are powered from a 5 V USB power supply (5 V @ min 500 mA).

Some more details about this project at  https://peterneufeld.wordpress.com/2021/09/29/eco2-telegram-bot/

The eCO2-Sensor CCS811
The CCS811 sensor calculates the eCO2-level  by meassuring the tVOC (total Volatile Organic Compound)  of exhaled air that may be  "polluted" by the human lungs. It can not return the absolute CO2-level!
 Additionally the sensors sensitivity changes over the time and under different environmental conditions. 
 This cheap type of sensor searches a relative  "good air condition"-baseline  by finding the "best air condition" in a longer interval and  then assuming the sensor being  in fresh unpolluted air at that time . 
 BUT: The CCS811 does not store this value by itself..
 
 The sensor needs two conditions for  giving reliable values: 
  • a one-time burn-in-time of about 48 hours
  • a minimum run-in of about 20 minutes after each cold start.

 Software
The BASIC program was created with and runs in  ANNEX32 -  a BASIC interpreter for ESP32. Minimum required version is V1.435 (version without Bluetooth but with TELEGRAM-support) 
  • The program measures the eCO2 level once per second.
  • The air eCO2 status gets  categorized as GREEN, YELLOW or RED condition.
  • The build-in NeoPixel-Matrix locally indicates the condition by its color.
  • A Webinterface displays the eCO2 value  and the category in the (W)LAN.
  • The Category  and eCO2 value can be requested via TELEGRAM messenger as the program works as a  TELEGRAM_BOT
  • a TELEGRAM-Alert is send to the last TELEGRAM-chat_id,  if condition is indicated as RED. 
  • The baseline value can be stored  manually by pressing the front button of the ATOM-module.

 The Telegram-BOT will respond 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

 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 bot in your telegram APP. This will provide your personal  TELEGRAM_TOKEN and a bot name for the BASIC-program code. that can be inserted in  the  BASIC-code lines  to set the appropriate variables

The ANNEX32-BASIC code  follows here for a better understanding of the functions.  
It is also attached as a file eCO2_BOT_V2_2.txt. This file  can be pasted into the editor of ANNEX32 and stored into the ESP32 module as an autorun-file in e.g. /default.bas
 
DO NOT FORGET TO INSERT YOUR OWN TELEGRAM TOKEN 
   

'#######################################################################
' eCO2-Sensor 
' ##################
'# 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 humans is proportional to their exhaled CO2.
'
'# USED HARDWARE:
'  - ESP32-device best: M%Stack "ATOM lite" or "ATOM matrix" with buit-in NeoPixel(s)
'  - CCS811 eCO2-TVOC-Sensor at I2C-pins of the  ESP32
'  - switch-button (e.g. the front button of M5Stack ATOM)
'
'# OUTPUT of eCO2-ppm  via ...
'- NEOPIXEL_LED with at least 1 pixel to indicate the range [GREEN|YELLOW|RED] of eCO2 ppm
'- WebInterface to display eCO2-ppm  and Condition [GREEN|YELLOW|RED]
'- TELEGRAM-BOT  message-on-demand with eCO2-ppm and Condition [GREEN|YELLOW|RED]
'- TELEGRAM-BOT  alert message, if condition reaches RED eCO2-range for more than x measures values

'# Saves the  BASELINEof 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 currentt CHAT_ID if RED-condition

'# TELEGRAM-BOT commands (with or without leading /) :
' /i => return local IP-Settings
' /s => store the current baseline representing "clean-air-condition"
'      in file  /baeline.txt
' /r => restore the baseline from file  /baeline.txt
' /e or [any other character] will return the  eCO2-ppm-value
'    and the condition [GREEN|YELLOW|RED]

'#######################################################################
' Peter Neufeld 2021/09; DB9JG@me.com
Version$           = "V2.2"

'------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 conditions 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

TELEGRAM_TOKEN$        = "xxxxxxxxxx:AAHQ3qTIj248QCU0ao_GcetggK3l_nhANj4"
TELEGRAM_BOTNAME$      = "@xxxxxx_eCO2_BOT"
TELEGRAM_MSG_Threshold = 5 'Message if bad-air-condition for too long

TELEGRAM_MAX_FAILED_COUNTER = 0
TELEGRAM_MAX_FAILED         = 10
TELEGRAM_is_still_running   = 0

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

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   'this is the main loop
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

'===Send a telegram alert message to latest CHAT_ID if too many RED conditions-----
IF condition$ = "RED"  and CHAT_ID$ <>"" then
  RED_COUNT = RED_COUNT +1
else   'reset counter if good condition return before reaching threshold
  RED_COUNT = 0
endif
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 longer
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:
'-------------
cls
autorefresh 1000
'create the textbox in the html page
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:| + CONDITION$ + |">|
A$ = A$ + "<H1>Condition: "+ CONDITION$+ "</H1>"
A$ = A$ + "</span>"
html A$
return

'####################################################################################
SHOW_eCO2:
'---------
x=5-x 'toggle the brigthness (0 or 5 up) just to show some activity
CONDITION_OLD$=CONDITION$
select case eCO2
  case 0 to  LL_YELLOW                    'GREEN
    R=0 : G=x+20  : B=0
    CONDITION$ = "GREEN"
    LL_RED_HYST = 0
  case LL_YELLOW to (LL_RED - LL_RED_HYST)'YELLOW
    R=x+10 : G=x+10 : B=0
    CONDITION$ = "YELLOW"
    LL_RED_HYST = 0
  CASE (LL_RED - LL_RED_HYST)to 99999      'RED
    R=x+20 : G=0 : B=0
    CONDITION$ = "RED"
    LL_RED_HYST = (LL_RED * HYST)  'set a hysteresis to stabilize  the condition
End  select

neo.strip 0, NEO_NUM, 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

'front-button to  inizialize a save-baseline-to-file
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
R=2 : G=2 : B=2  'initial colors for neopixel
neo.setup NEO_PIN, NEO_NUM
neo.strip 0,NEO_NUM,R,G,B

'CCS811 eCO2-Sensor
if ccs811.Setup(&h5a) <> 0 then
  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_NUM,0,0,150   'set Neopixels 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_NUM,R,G,B     'Back to old colors
return

'####################################################################################
CCS811_RESTORE_BL_FROM_FILE:
'---------------------------
BASELINE$  = "39103"  'default 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
'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 CHAT_ID$="" then return

tt$ = "RED-ALERT condition  for " + LOCATION$ +" at "+ time$ + ": eCO2 = " + str_eCO2$  + "  Condition: " + CONDITION$

neo.strip 0,NEO_NUM,30,30,30   'white Neopixels to  indicate a TELEGRAM transmission
WLOG telegram.sendmessage$(val(CHAT_ID$),tt$ )
WLOG tt$
neo.strip 0,NEO_NUM,R,G,B      'back to former condition

TELEGRAM_is_still_running = 0
onwgetasync        TELEGRAM_asynco
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 CHAT_ID$=c$

tt$ = LOCATION$ +" at "+ time$ + ": eCO2 = " + str_eCO2$  + "  Condition: " + CONDITION$

text$ = json$(r$, "text")
if (text$ <> "not found") then
  text$=replace$(text$,"/","") ' now the command may, but must not include a  leading /
  select case left$(lcase$(text$),1)
    case "i" : tt$=" Local IP-setting is " + IP$
    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"
  end select
  neo.strip 0,NEO_NUM,30,30,30   'white Neopixels to  indicate a TELEGRAM transmission
  WLOG telegram.sendmessage$(val(CHAT_ID$),tt$ )
  neo.strip 0,NEO_NUM,R,G,B      'back to former condition
  
end if
TELEGRAM_is_still_running = 0
onwgetasync        TELEGRAM_asynco
return
 '####################################################################################