Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for ENS160 digital multi-gas sensor with multiple IAQ data (TVOC, eCO2, AQI) #371

Merged
merged 5 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Hardware support is provided by specific GPIO, Sensor and Stream modules. It's e
- BME680 temperature, humidity and pressure sensor (`bme680`)
- DHT11/DHT22/AM2302 temperature and humidity sensors (`dht22`)
- DS18S20/DS1822/DS18B20/DS1825/DS28EA00/MAX31850K temperature sensors (`ds18b`)
- ENS160 digital multi-gas sensor with multiple IAQ data (TVOC, eCO2, AQI) (`ens160`)
- HCSR04 ultrasonic range sensor (connected to the Raspberry Pi on-board GPIO) (`hcsr04`)
- INA219 DC current sensor (`ina219`)
- LM75 temperature sensor (`lm75`)
Expand Down
125 changes: 125 additions & 0 deletions mqtt_io/modules/sensor/ens160.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""
ENS160 Air Quality Sensor

sensor_modules:
- name: ens160
module: ens160
chip_addr: 0x53
temperature_compensation: 25
humidity_compensation: 50

sensor_inputs:
- name: air_quality
module: ens160
interval: 10
digits: 0
type: aqi

- name: volatile_organic_compounds
module: ens160
interval: 10
digits: 0
type: tvoc

- name: eco2
module: ens160
interval: 10
digits: 0
type: eco2
"""

from typing import cast

from ...types import CerberusSchemaType, ConfigType
from . import GenericSensor

DEFAULT_CHIP_ADDR = 0x53
DEFAULT_TEMPERATURE_COMPENSATION = 25
DEFAULT_HUMIDITY_COMPENSATION = 50


REQUIREMENTS = ("adafruit-circuitpython-ens160",)
CONFIG_SCHEMA: CerberusSchemaType = {
"chip_addr": dict(
type="integer", required=False, empty=False, default=DEFAULT_CHIP_ADDR
),
"temperature_compensation": dict(
type="float",
required=False,
empty=False,
default=DEFAULT_TEMPERATURE_COMPENSATION,
),
"humidity_compensation": dict(
type="float",
required=False,
empty=False,
default=DEFAULT_HUMIDITY_COMPENSATION,
),
}


class Sensor(GenericSensor):
"""
Implementation of Sensor class for the ENS160 sensor using adafruit-circuitpython-ens160.

Mesures:
AQI: The air quality index calculated on the basis of UBA
Return value: 1-Excellent, 2-Good, 3-Moderate, 4-Poor, 5-Unhealthy

TVOC: Total Volatile Organic Compounds concentration
Return value range: 0–65000, unit: ppb

CO2 equivalent concentration calculated according to the detected data of VOCs and hydrogen
Return value range: 400–65000, unit: ppm

Five levels: Excellent(400 - 600), Good(600 - 800), Moderate(800 - 1000),
Poor(1000 - 1500), Unhealthy(> 1500)

NB: Need to think about how to handle the ambient_temp and relative_humidity values as
they are currently hard-coded defaults that can be overridden by user configuration.
Ideally these values would be read from a separate temperature/humdity sensor.
"""

SENSOR_SCHEMA: CerberusSchemaType = {
"type": dict(
type="string",
required=False,
empty=False,
default="aqi",
allowed=["aqi", "tvoc", "eco2"],
),
}

def setup_module(self) -> None:
# pylint: disable=import-outside-toplevel,import-error
import adafruit_ens160 # type: ignore
import board # type: ignore

self.adafruit_ens160_module = adafruit_ens160
i2c = board.I2C() # uses board.SCL and board.SDA
self.ens160 = adafruit_ens160.ENS160(i2c, address=self.config["chip_addr"])
self.ens160.temperature_compensation = self.config["temperature_compensation"]
self.ens160.humidity_compensation = self.config["humidity_compensation"]

def get_value(self, sens_conf: ConfigType) -> float:
"""Return the sensor value in the configured type."""

# data_validity values:
# NORMAL_OP - Normal operation,
# WARM_UP - Warm-Up phase, first 3 minutes after power-on.
# START_UP - Initial Start-Up phase, first full hour of operation after initial power-on.
# Only once in the sensor’s lifetime.
# INVALID_OUT - Invalid output
# note: Note that the status will only be stored in the non-volatile memory after an initial
# 24h of continuous operation. If unpowered before conclusion of said period, the
# ENS160 will resume "Initial Start-up" mode after re-powering.
if self.ens160.data_validity == self.adafruit_ens160_module.INVALID_OUT:
raise RuntimeError("ENS160 sensor is returning invalid output")

sens_type = sens_conf["type"]
return cast(
int,
dict(aqi=self.ens160.AQI, tvoc=self.ens160.TVOC, eco2=self.ens160.eCO2)[
sens_type
],
)
Loading