From 1c6454047773302bb49a6164007208f8bd0567eb Mon Sep 17 00:00:00 2001 From: Jens Thomas Date: Mon, 10 Jun 2024 12:52:01 +0100 Subject: [PATCH 1/4] ENS160 sensor based on adafruit circultpython --- mqtt_io/modules/sensor/ens160.py | 130 +++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 mqtt_io/modules/sensor/ens160.py diff --git a/mqtt_io/modules/sensor/ens160.py b/mqtt_io/modules/sensor/ens160.py new file mode 100644 index 00000000..2c40e52a --- /dev/null +++ b/mqtt_io/modules/sensor/ens160.py @@ -0,0 +1,130 @@ +""" + +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 = { + # "i2c_bus_num": dict(type="integer", required=True, empty=False), + "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 (eCO2 – Equivalent CO2) + 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 + # i2c = busio.I2C(board.SCL, board.SDA) + # i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller + 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.""" + + status = self.ens160.data_validity + # Return value: + # 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 status == self.adafruit_ens160_module.INVALID_OUT: + raise RuntimeError("ENS160 sensor is returning invalid output") + + data = self.ens160.read_all_sensors() + # print("Sensor resistances (ohms):", data["Resistances"]) + sens_type = sens_conf["type"] + return cast( + int, dict(aqi=data["AQI"], tvoc=data["TVOC"], co2=data["eCO2"])[sens_type] + ) From f95f034392efb342ad77c60ed13ce7773a8d5c50 Mon Sep 17 00:00:00 2001 From: linucks Date: Mon, 10 Jun 2024 13:37:41 +0100 Subject: [PATCH 2/4] Updates from raspberry pi --- mqtt_io/modules/sensor/ens160.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/mqtt_io/modules/sensor/ens160.py b/mqtt_io/modules/sensor/ens160.py index 2c40e52a..76370e40 100644 --- a/mqtt_io/modules/sensor/ens160.py +++ b/mqtt_io/modules/sensor/ens160.py @@ -99,10 +99,10 @@ def setup_module(self) -> None: import board # type: ignore self.adafruit_ens160_module = adafruit_ens160 - + i2c = board.I2C() # uses board.SCL and board.SDA - # i2c = busio.I2C(board.SCL, board.SDA) # i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller + 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"] @@ -110,8 +110,7 @@ def setup_module(self) -> None: def get_value(self, sens_conf: ConfigType) -> float: """Return the sensor value in the configured type.""" - status = self.ens160.data_validity - # Return value: + # 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. @@ -119,12 +118,10 @@ def get_value(self, sens_conf: ConfigType) -> float: # 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 status == self.adafruit_ens160_module.INVALID_OUT: + if self.ens160.data_validity == self.adafruit_ens160_module.INVALID_OUT: raise RuntimeError("ENS160 sensor is returning invalid output") - data = self.ens160.read_all_sensors() - # print("Sensor resistances (ohms):", data["Resistances"]) sens_type = sens_conf["type"] return cast( - int, dict(aqi=data["AQI"], tvoc=data["TVOC"], co2=data["eCO2"])[sens_type] + int, dict(aqi=self.ens160.AQI, tvoc=self.ens160.TVOC, eco2=self.ens160.eCO2)[sens_type] ) From bcf575c27bc48648e6edc090eb122ca01e0bd8f7 Mon Sep 17 00:00:00 2001 From: Jens Thomas Date: Mon, 10 Jun 2024 13:42:00 +0100 Subject: [PATCH 3/4] Added support for ENS160 --- README.md | 1 + mqtt_io/modules/sensor/ens160.py | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index db86a8d1..b092319e 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 index 76370e40..16710135 100644 --- a/mqtt_io/modules/sensor/ens160.py +++ b/mqtt_io/modules/sensor/ens160.py @@ -42,7 +42,6 @@ REQUIREMENTS = ("adafruit-circuitpython-ens160",) CONFIG_SCHEMA: CerberusSchemaType = { - # "i2c_bus_num": dict(type="integer", required=True, empty=False), "chip_addr": dict( type="integer", required=False, empty=False, default=DEFAULT_CHIP_ADDR ), @@ -99,7 +98,7 @@ def setup_module(self) -> None: import board # type: ignore self.adafruit_ens160_module = adafruit_ens160 - + i2c = board.I2C() # uses board.SCL and board.SDA # i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller @@ -123,5 +122,8 @@ def get_value(self, sens_conf: ConfigType) -> float: sens_type = sens_conf["type"] return cast( - int, dict(aqi=self.ens160.AQI, tvoc=self.ens160.TVOC, eco2=self.ens160.eCO2)[sens_type] + int, + dict(aqi=self.ens160.AQI, tvoc=self.ens160.TVOC, eco2=self.ens160.eCO2)[ + sens_type + ], ) From c0a78ccef5044472d77944cbd943d62c4718bda5 Mon Sep 17 00:00:00 2001 From: Jens Thomas Date: Thu, 20 Jun 2024 09:14:41 +0100 Subject: [PATCH 4/4] Changes required by pylint It now passes all tests --- mqtt_io/modules/sensor/ens160.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/mqtt_io/modules/sensor/ens160.py b/mqtt_io/modules/sensor/ens160.py index 16710135..526f1906 100644 --- a/mqtt_io/modules/sensor/ens160.py +++ b/mqtt_io/modules/sensor/ens160.py @@ -1,5 +1,4 @@ """ - ENS160 Air Quality Sensor sensor_modules: @@ -27,7 +26,6 @@ interval: 10 digits: 0 type: eco2 - """ from typing import cast @@ -71,7 +69,7 @@ class Sensor(GenericSensor): 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 (eCO2 – Equivalent CO2) + 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), @@ -98,10 +96,7 @@ def setup_module(self) -> None: import board # type: ignore self.adafruit_ens160_module = adafruit_ens160 - i2c = board.I2C() # uses board.SCL and board.SDA - # i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller - 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"] @@ -112,11 +107,12 @@ def get_value(self, sens_conf: ConfigType) -> float: # 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. + # 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. + # 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")