Automating Indoor Air Quality with Adafruit FunHouse CO2 Monitoring
2021-05-24 | By Nate_Larson
License: See Original Project
We’ve all been spending a lot more time in our homes over the past year, and with a heightened awareness of health, it is only a matter of time before one ponders the air quality to which we are subjecting ourselves. Good air quality is imperative to overall health. Maintaining good air quality involves monitoring the indoor carbon dioxide (CO2) level, which provides a good indication of air quality as stale indoor air is replaced with fresh air from outside.
In this project, I will demonstrate how one can monitor indoor air quality, and then automate ventilation equipment to ensure optimal indoor air quality.
Project Overview
In this project, we will monitor the indoor CO2 level to determine air quality. To monitor the CO2 level, we will use the Adafruit SCD-30 breakout board, which features the Sensirion SCD30 NDIR CO2 sensor module with integrated temperature and humidity sensing for greater accuracy.
The sensor board communicates with the Adafruit FunHouse board over I2C using the solder-free STEMMA QT / Qwiic connector interface. The FunHouse board is powered by an Espressif ESP32-S2-WROVER Wi-Fi MCU module, which polls the sensor for updated data. Then it displays the sensor readings using the onboard screen and publishes these readings to an MQTT server within an instance of Home Assistant running on a Raspberry Pi 4 Model B with 2GB of SDRAM.
The Raspberry Pi logs and monitors these readings, then when the sensor values surpass a threshold, it sends a signal via API to an Adafruit ESP32 Feather Huzzah board. This board then sets a pin, closing the relays on two attached FeatherWing boards, one of which closes the circuit to turn on an energy recovery ventilation unit (ERV) to exchange indoor air with fresh air from outside, while the second relay activates the HVAC fan to circulate the fresh air throughout the house. Once air quality improves and sensor readings drop to a predefined value, the Raspberry Pi turns off the ERV and HVAC fan by signaling to open the relays.
Materials
- (1x) Adafruit FunHouse - WiFi Home Automation Development Board
- (1x) Adafruit SCD-30 - NDIR CO2 Temperature and Humidity Sensor - STEMMA QT / Qwiic
- (1x) Adafruit HUZZAH32 – ESP32 Feather Board
- (2x) Adafruit Non-Latching Mini Relay FeatherWing
- (1x) STEMMA QT / Qwiic JST SH 4-pin Cable
- (2x) Stacking Headers for Feather - 12-pin and 16-pin female headers
- (1x) Mini Self Adhesive 170 Tie Point Solderless Breadboard
Prerequisites
This project assumes you already have Home Assistant with the ESPHome add-on setup and running on your network. If you need information on how to do so, please see Smart Home Automation, Iteration 2: Home Assistant and ESPHome for information on how to do this. You must also have your FunHouse board already set up and communicating with your Home Assistant server via MQTT as per the learn guide Using the Adafruit FunHouse with Home Assistant.
Hardware Prep
Begin by soldering the male headers to the Feather Huzzah board. Place the headers into a breadboard to keep them aligned while they are soldered. Then place the board on top of the headers, solder the headers to the board, and remove the assembly from the breadboard.
Place a spare set of male headers into the breadboard with the same spacing as the outer relay FeatherWing board.
Place a set of the female headers upside-down onto these headers and place the relay FeatherWing upside-down on the headers. Again, this keeps the headers aligned while the board is being soldered. Now solder the headers on this board and then place and solder the terminal blocks to the relay board. Since we are stacking two of these relay boards, it is best to trim the excess leads from the terminal block after they are soldered to ensure the top board doesn’t short to the board below. Repeat this process for the second Relay FeatherWing.
Finally, for each relay board, solder a short jumper wire from the signal pin to one of the GPIO pins. In this example, we will use pin 4. Remember, we need these relays to always switch together, so ensure you select the same GPIO pin for both relay FeatherWing boards.
Hardware setup for the FunHouse board is extremely easy. Simply use a STEMMA QT / Qwiic cable to connect the CO2 sensor board to the mainboard via the STEMMA QT / Qwiic connectors on each of these boards.
CircuitPython Code
You will find the code is very similar to that of the Using the Adafruit FunHouse with Home Assistant project, but with a few additions for the CO2 sensor. To interface with the CO2 sensor, you will need to download the CircuitPython library bundle and add the adafruit_scd30 file to the lib folder on your board along with all the library files for the aforementioned project.
Below is the CircuitPython code for the Adafruit FunHouse for our project.
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
# SPDX-FileCopyrightText: Copyright (c) 2021 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
import board
import adafruit_scd30
import json
from adafruit_display_shapes.circle import Circle
from adafruit_funhouse import FunHouse
PUBLISH_DELAY = 60
ENVIRONMENT_CHECK_DELAY = 5
ENABLE_PIR = False
ENABLE_PIR = False
MQTT_TOPIC = "funhouse/state"
LIGHT_STATE_TOPIC = "funhouse/light/state"
LIGHT_COMMAND_TOPIC = "funhouse/light/set"
INITIAL_LIGHT_COLOR = 0x008000
USE_FAHRENHEIT = True
try:
from secrets import secrets
except ImportError:
print("WiFi secrets are kept in secrets.py, please add them there!")
raise
i2c = board.I2C() # uses board.SCL and board.SDA
scd = adafruit_scd30.SCD30(board.I2C())
funhouse = FunHouse(default_bg=0x0F0F00)
funhouse.peripherals.dotstars.fill(INITIAL_LIGHT_COLOR)
funhouse.display.show(None)
funhouse.add_text(
text="Temperature:",
text_position=(20, 10),
text_color=0xFF8888,
text_font="fonts/Arial-18.pcf",
)
temp_label = funhouse.add_text(
text_position=(120, 40),
text_anchor_point=(0.5, 0.5),
text_color=0xFFFF00,
text_font="fonts/Arial-18.pcf",
)
funhouse.add_text(
text="Humidity:",
text_position=(20, 70),
text_color=0x8888FF,
text_font="fonts/Arial-18.pcf",
)
hum_label = funhouse.add_text(
text_position=(120, 100),
text_anchor_point=(0.5, 0.5),
text_color=0xFFFF00,
text_font="fonts/Arial-18.pcf",
)
funhouse.add_text(
text="Pressure:",
text_position=(20, 130),
text_color=0xFF88FF,
text_font="fonts/Arial-18.pcf",
)
pres_label = funhouse.add_text(
text_position=(120, 160),
text_anchor_point=(0.5, 0.5),
text_color=0xFFFF00,
text_font="fonts/Arial-18.pcf",
)
funhouse.add_text(
text="CO2:",
text_position=(20, 190),
text_color=0xFF88FF,
text_font="fonts/Arial-18.pcf",
)
co2_label = funhouse.add_text(
text_position=(120, 220),
text_anchor_point=(0.5, 0.5),
text_color=0xFFFF00,
text_font="fonts/Arial-18.pcf",
)
funhouse.display.show(funhouse.splash)
status = Circle(229, 10, 10, fill=0xFF0000, outline=0x880000)
funhouse.splash.append(status)
def update_enviro():
global environment
if scd.data_available:
temp = scd.temperature
unit = "C"
if USE_FAHRENHEIT:
temp = temp * (9 / 5) + 32
unit = "F"
environment["temperature"] = temp
environment["pressure"] = funhouse.peripherals.pressure
environment["humidity"] = scd.relative_humidity
environment["co2"] = scd.CO2
environment["light"] = funhouse.peripherals.light
funhouse.set_text("{:.1f}{}".format(environment["temperature"], unit), temp_label)
funhouse.set_text("{:.1f}%".format(environment["humidity"]), hum_label)
funhouse.set_text("{}hPa".format(environment["pressure"]), pres_label)
funhouse.set_text("{:.0f}PPM".format(environment["co2"]), co2_label)
def connected(client, userdata, result, payload):
status.fill = 0x00FF00
status.outline = 0x008800
print("Connected to MQTT! Subscribing...")
client.subscribe(LIGHT_COMMAND_TOPIC)
def disconnected(client):
status.fill = 0xFF0000
status.outline = 0x880000
def message(client, topic, payload):
print("Topic {0} received new value: {1}".format(topic, payload))
if topic == LIGHT_COMMAND_TOPIC:
settings = json.loads(payload)
if settings["state"] == "on":
if "brightness" in settings:
funhouse.peripherals.dotstars.brightness = settings["brightness"] / 255
else:
funhouse.peripherals.dotstars.brightness = 0.3
if "color" in settings:
funhouse.peripherals.dotstars.fill(settings["color"])
else:
funhouse.peripherals.dotstars.brightness = 0
publish_light_state()
def publish_light_state():
funhouse.peripherals.led = True
output = {
"brightness": round(funhouse.peripherals.dotstars.brightness * 255),
"state": "on" if funhouse.peripherals.dotstars.brightness > 0 else "off",
"color": funhouse.peripherals.dotstars[0],
}
# Publish the Dotstar State
print("Publishing to {}".format(LIGHT_STATE_TOPIC))
funhouse.network.mqtt_publish(LIGHT_STATE_TOPIC, json.dumps(output))
funhouse.peripherals.led = False
# Initialize a new MQTT Client object
funhouse.network.init_mqtt(
secrets["mqtt_broker"],
secrets["mqtt_port"],
secrets["mqtt_username"],
secrets["mqtt_password"],
)
funhouse.network.on_mqtt_connect = connected
funhouse.network.on_mqtt_disconnect = disconnected
funhouse.network.on_mqtt_message = message
print("Attempting to connect to {}".format(secrets["mqtt_broker"]))
funhouse.network.mqtt_connect()
last_publish_timestamp = None
last_peripheral_state = {
"button_up": funhouse.peripherals.button_up,
"button_down": funhouse.peripherals.button_down,
"button_sel": funhouse.peripherals.button_sel,
"captouch6": funhouse.peripherals.captouch6,
"captouch7": funhouse.peripherals.captouch7,
"captouch8": funhouse.peripherals.captouch8,
}
if ENABLE_PIR:
last_peripheral_state["pir_sensor"] = funhouse.peripherals.pir_sensor
environment = {}
update_enviro()
last_environment_timestamp = time.monotonic()
# Provide Initial light state
publish_light_state()
while True:
if not environment or (
time.monotonic() - last_environment_timestamp > ENVIRONMENT_CHECK_DELAY
):
update_enviro()
last_environment_timestamp = time.monotonic()
output = environment
peripheral_state_changed = False
for peripheral in last_peripheral_state:
current_item_state = getattr(funhouse.peripherals, peripheral)
output[peripheral] = "on" if current_item_state else "off"
if last_peripheral_state[peripheral] != current_item_state:
peripheral_state_changed = True
last_peripheral_state[peripheral] = current_item_state
if funhouse.peripherals.slider is not None:
output["slider"] = funhouse.peripherals.slider
peripheral_state_changed = True
# Every PUBLISH_DELAY, write temp/hum/press/light or if a peripheral changed
if (
last_publish_timestamp is None
or peripheral_state_changed
or (time.monotonic() - last_publish_timestamp) > PUBLISH_DELAY
):
funhouse.peripherals.led = True
print("Publishing to {}".format(MQTT_TOPIC))
funhouse.network.mqtt_publish(MQTT_TOPIC, json.dumps(output))
funhouse.peripherals.led = False
last_publish_timestamp = time.monotonic()
# Check any topics we are subscribed to
funhouse.network.mqtt_loop(0.5)
Changes to the code from the base Adafruit project include:
- Inclusion of CO2 sensor library
- Reducing label and sensor value text sizes displayed on the screen to make room for the additional CO2 sensor value
- Added display labels and variable for CO2 sensor value
- Temperature and humidity were changed to use the values reported by the SCD30 sensor, as they seemed more accurate than those reported by the FunHouse onboard sensor
- SCD sensor values only update when the sensor indicates it has data available
Don’t forget to add the CO2 sensor to your Home Assistant configuration file so this sensor data can be logged and displayed by the server.
Setting up the ESP32 Feather Huzzah
The ESP32 Feather Huzzah is programmed using the ESPHome add-on in Home Assistant. If you need information on how to set this up, or how to create a new node using ESPHome, please see the Smart Home Automation, Iteration 2: Home Assistant and ESPHome project. Once your new node is created, we need to add the code to identify this node as having a device class of “switch”, name that switch, and finally designate which GPIO the relay is attached to. You can see the YAML code for this below.
esphome:
name: erv_controller
platform: ESP32
board: featheresp32
wifi:
ssid: "WiFi Network Name"
password: "WiFi Password"
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Erv Controller Fallback Hotspot"
password: "Password"
captive_portal:
# Enable logging
logger:
# Enable Home Assistant API
api:
password: "API Password"
ota:
password: "OTA Password"
switch:
- platform: gpio
name: "Relay"
pin: 4
Once this code is flashed to the Feather, you can begin wiring the relay boards into your existing system. My current ERV controller is a simple timer switch mounted near the ERV in the mechanical room.
The timer switch is powered by 24VAC provided by the HVAC system and has the below wire connections.
The new controller does not require the Common wire, so this was disconnected and insulated. The first relay board will control the HVAC fan, so the HVAC control board fan wire is connected to the common (COM) pin of the relay terminal block, with the HVAC R wire connected to the normally open (NO) relay connection and the thermostat fan wire connected to the normally closed (NC) position of the terminal block. This allows the thermostat to continue to control the HVAC fan whenever the relay is not activated; meanwhile, if the relay is closed, the HVAC fan will turn on.
The second relay board will close the switch loop for the ERV. One wire is connected to the common pin on the terminal block while the second wire is connected to the normally open pin.
Once the relay connections are made, the Feather and FeatherWing boards can be stacked together and powered via a USB power supply. I chose to temporarily mount a small breadboard using some 3M VHB tape and plug the assembly into it until I can 3D print a proper enclosure for the assembly. This also provides easy access to the other GPIO on the board so I can experiment with adding additional hardware and features and allows visibility of the relay indicator LEDs to verify proper operation.
Creating the Home Assistant Automation
Now that our sensor data is being recorded, and we have enabled Home Assistant control of our ventilation system, we need to automate our indoor air quality to ensure proper ventilation.
We will create two separate automation routines to control the ERV using the values of the CO2 sensor. On the automation page within the Home Assistant server web interface, click the “ADD AUTOMATION” button, then select the “START WITH AN EMPTY AUTOMATION” option.
The first Automation will turn on the ventilation system, so I named it ERVon. Select “Numeric State” for the trigger type, and then sensor.co2 for the trigger entity. We want the ventilation system to activate when the sensor reads above 1000ppm, so put 1000 as the “Above” numeric value. Finally, since the sensor value fluctuates a bit as ambient conditions change, we only want to activate the ventilation system if the reading has been consistently above 1000ppm for five minutes, so set the “For” parameter to 00:05:00.
Now that our trigger is defined, we can define the automation’s action. Set the action type to “Device” and select “erv_controller” as the target device. Finally, set the action to “Turn on ERV” and save the automation.
If you prefer to create the automation by editing the yaml code as opposed to using the visual editor above, you can find the automation code below.
alias: ERVon
description: ''
trigger:
- platform: numeric_state
entity_id: sensor.co2
above: '1000'
for: '00:05:00'
condition: []
action:
- type: turn_on
device_id: 5d7fae794cf960b1c53a1a5f803f3af3
entity_id: switch.relay
domain: switch
mode: single
Now that our trigger is defined, we can define the automation’s action. Set the action type to “Device” and select “erv_controller” as the target device. Finally, set the action to “Turn on ERV” and save the automation.
alias: ERVoff
description: ''
trigger:
- platform: numeric_state
entity_id: sensor.co2
below: '700'
for: '00:05:00'
condition: []
action:
- type: turn_off
device_id: 5d7fae794cf960b1c53a1a5f803f3af3
entity_id: switch.relay
domain: switch
mode: single
Going Further
With this complete, we now have automated our ventilation system to assure we have optimal indoor air quality. This project is ripe with possibilities for additional features, so I anticipate adding VOC sensing to this in the future, along with setting the LEDs on the FunHouse board to change color according to CO2 sensor values and whether the ERV is running, among other ideas.
If you would like to learn more about CO2 level sensing and indoor air quality, Make: just published a great discussion about this with Guido Burger where they go into greater detail on the importance of indoor air quality, the differences among CO2 sensors, and various monitoring devices created. You can find this on the Make website here: https://makezine.com/2021/05/17/the-what-how-and-why-of-co-monitoring/
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.
Visit TechForum