From 88cff8505b71cba165c098646f5f89859a1aed48 Mon Sep 17 00:00:00 2001 From: Jens Thomas Date: Tue, 9 Jul 2024 19:51:49 +0100 Subject: [PATCH] Support for ENS160 digital multi-gas sensor with multiple IAQ data (TVOC, eCO2, AQI) (#371) * ENS160 sensor based on adafruit circultpython --- README.md | 1 + mqtt_io/modules/sensor/ens160.py | 125 +++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 mqtt_io/modules/sensor/ens160.py diff --git a/README.md b/README.md index 67ce611c..81f2dcbc 100644 --- a/README.md +++ b/README.md @@ -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`) diff --git a/mqtt_io/modules/sensor/ens160.py b/mqtt_io/modules/sensor/ens160.py new file mode 100644 index 00000000..526f1906 --- /dev/null +++ b/mqtt_io/modules/sensor/ens160.py @@ -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 + ], + )