Add ENS160 Sensor (#4243)

Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
Co-authored-by: mrtoy-me <118446898+mrtoy-me@users.noreply.github.com>
This commit is contained in:
Vincent Schmandt 2023-11-24 05:31:07 +01:00 committed by GitHub
parent 5c31bec8c2
commit f456603c1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 478 additions and 5 deletions

View File

@ -90,6 +90,7 @@ esphome/components/duty_time/* @dudanov
esphome/components/ee895/* @Stock-M esphome/components/ee895/* @Stock-M
esphome/components/ektf2232/* @jesserockz esphome/components/ektf2232/* @jesserockz
esphome/components/emc2101/* @ellull esphome/components/emc2101/* @ellull
esphome/components/ens160/* @vincentscode
esphome/components/ens210/* @itn3rd77 esphome/components/ens210/* @itn3rd77
esphome/components/esp32/* @esphome/core esphome/components/esp32/* @esphome/core
esphome/components/esp32_ble/* @Rapsssito @jesserockz esphome/components/esp32_ble/* @Rapsssito @jesserockz

View File

@ -0,0 +1 @@
CODEOWNERS = ["@vincentscode"]

View File

@ -0,0 +1,321 @@
// ENS160 sensor with I2C interface from ScioSense
//
// Datasheet: https://www.sciosense.com/wp-content/uploads/documents/SC-001224-DS-7-ENS160-Datasheet.pdf
//
// Implementation based on:
// https://github.com/sciosense/ENS160_driver
#include "ens160.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace ens160 {
static const char *const TAG = "ens160";
static const uint8_t ENS160_BOOTING = 10;
static const uint16_t ENS160_PART_ID = 0x0160;
static const uint8_t ENS160_REG_PART_ID = 0x00;
static const uint8_t ENS160_REG_OPMODE = 0x10;
static const uint8_t ENS160_REG_CONFIG = 0x11;
static const uint8_t ENS160_REG_COMMAND = 0x12;
static const uint8_t ENS160_REG_TEMP_IN = 0x13;
static const uint8_t ENS160_REG_DATA_STATUS = 0x20;
static const uint8_t ENS160_REG_DATA_AQI = 0x21;
static const uint8_t ENS160_REG_DATA_TVOC = 0x22;
static const uint8_t ENS160_REG_DATA_ECO2 = 0x24;
static const uint8_t ENS160_REG_GPR_READ_0 = 0x48;
static const uint8_t ENS160_REG_GPR_READ_4 = ENS160_REG_GPR_READ_0 + 4;
static const uint8_t ENS160_COMMAND_NOP = 0x00;
static const uint8_t ENS160_COMMAND_CLRGPR = 0xCC;
static const uint8_t ENS160_COMMAND_GET_APPVER = 0x0E;
static const uint8_t ENS160_OPMODE_RESET = 0xF0;
static const uint8_t ENS160_OPMODE_IDLE = 0x01;
static const uint8_t ENS160_OPMODE_STD = 0x02;
static const uint8_t ENS160_DATA_STATUS_STATAS = 0x80;
static const uint8_t ENS160_DATA_STATUS_STATER = 0x40;
static const uint8_t ENS160_DATA_STATUS_VALIDITY = 0x0C;
static const uint8_t ENS160_DATA_STATUS_NEWDAT = 0x02;
static const uint8_t ENS160_DATA_STATUS_NEWGPR = 0x01;
// helps remove reserved bits in aqi data register
static const uint8_t ENS160_DATA_AQI = 0x07;
void ENS160Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up ENS160...");
// check part_id
uint16_t part_id;
if (!this->read_bytes(ENS160_REG_PART_ID, reinterpret_cast<uint8_t *>(&part_id), 2)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();
return;
}
if (part_id != ENS160_PART_ID) {
this->error_code_ = INVALID_ID;
this->mark_failed();
return;
}
// set mode to reset
if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_RESET)) {
this->error_code_ = WRITE_FAILED;
this->mark_failed();
return;
}
delay(ENS160_BOOTING);
// check status
uint8_t status_value;
if (!this->read_byte(ENS160_REG_DATA_STATUS, &status_value)) {
this->error_code_ = READ_FAILED;
this->mark_failed();
return;
}
this->validity_flag_ = static_cast<ValidityFlag>((ENS160_DATA_STATUS_VALIDITY & status_value) >> 2);
if (this->validity_flag_ == INVALID_OUTPUT) {
this->error_code_ = VALIDITY_INVALID;
this->mark_failed();
return;
}
// set mode to idle
if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_IDLE)) {
this->error_code_ = WRITE_FAILED;
this->mark_failed();
return;
}
// clear command
if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_NOP)) {
this->error_code_ = WRITE_FAILED;
this->mark_failed();
return;
}
if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_CLRGPR)) {
this->error_code_ = WRITE_FAILED;
this->mark_failed();
return;
}
// read firmware version
if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_GET_APPVER)) {
this->error_code_ = WRITE_FAILED;
this->mark_failed();
return;
}
uint8_t version_data[3];
if (!this->read_bytes(ENS160_REG_GPR_READ_4, version_data, 3)) {
this->error_code_ = READ_FAILED;
this->mark_failed();
return;
}
this->firmware_ver_major_ = version_data[0];
this->firmware_ver_minor_ = version_data[1];
this->firmware_ver_build_ = version_data[2];
// set mode to standard
if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_STD)) {
this->error_code_ = WRITE_FAILED;
this->mark_failed();
return;
}
// read opmode and check standard mode is achieved before finishing Setup
uint8_t op_mode;
if (!this->read_byte(ENS160_REG_OPMODE, &op_mode)) {
this->error_code_ = READ_FAILED;
this->mark_failed();
return;
}
if (op_mode != ENS160_OPMODE_STD) {
this->error_code_ = STD_OPMODE_FAILED;
this->mark_failed();
return;
}
}
void ENS160Component::update() {
uint8_t status_value, data_ready;
if (!this->read_byte(ENS160_REG_DATA_STATUS, &status_value)) {
ESP_LOGW(TAG, "Error reading status register");
this->status_set_warning();
return;
}
// verbose status logging
ESP_LOGV(TAG, "Status: ENS160 STATAS bit 0x%x",
(ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS);
ESP_LOGV(TAG, "Status: ENS160 STATER bit 0x%x",
(ENS160_DATA_STATUS_STATER & (status_value)) == ENS160_DATA_STATUS_STATER);
ESP_LOGV(TAG, "Status: ENS160 VALIDITY FLAG 0x%02x", (ENS160_DATA_STATUS_VALIDITY & status_value) >> 2);
ESP_LOGV(TAG, "Status: ENS160 NEWDAT bit 0x%x",
(ENS160_DATA_STATUS_NEWDAT & (status_value)) == ENS160_DATA_STATUS_NEWDAT);
ESP_LOGV(TAG, "Status: ENS160 NEWGPR bit 0x%x",
(ENS160_DATA_STATUS_NEWGPR & (status_value)) == ENS160_DATA_STATUS_NEWGPR);
data_ready = ENS160_DATA_STATUS_NEWDAT & status_value;
this->validity_flag_ = static_cast<ValidityFlag>((ENS160_DATA_STATUS_VALIDITY & status_value) >> 2);
switch (validity_flag_) {
case NORMAL_OPERATION:
if (data_ready != ENS160_DATA_STATUS_NEWDAT) {
ESP_LOGD(TAG, "ENS160 readings unavailable - Normal Operation but readings not ready");
return;
}
break;
case INITIAL_STARTUP:
if (!this->initial_startup_) {
this->initial_startup_ = true;
ESP_LOGI(TAG, "ENS160 readings unavailable - 1 hour startup required after first power on");
}
return;
case WARMING_UP:
if (!this->warming_up_) {
this->warming_up_ = true;
ESP_LOGI(TAG, "ENS160 readings not available yet - Warming up requires 3 minutes");
this->send_env_data_();
}
return;
case INVALID_OUTPUT:
ESP_LOGE(TAG, "ENS160 Invalid Status - No Invalid Output");
this->status_set_warning();
return;
}
// read new data
uint16_t data_eco2;
if (!this->read_bytes(ENS160_REG_DATA_ECO2, reinterpret_cast<uint8_t *>(&data_eco2), 2)) {
ESP_LOGW(TAG, "Error reading eCO2 data register");
this->status_set_warning();
return;
}
if (this->co2_ != nullptr) {
this->co2_->publish_state(data_eco2);
}
uint16_t data_tvoc;
if (!this->read_bytes(ENS160_REG_DATA_TVOC, reinterpret_cast<uint8_t *>(&data_tvoc), 2)) {
ESP_LOGW(TAG, "Error reading TVOC data register");
this->status_set_warning();
return;
}
if (this->tvoc_ != nullptr) {
this->tvoc_->publish_state(data_tvoc);
}
uint8_t data_aqi;
if (!this->read_byte(ENS160_REG_DATA_AQI, &data_aqi)) {
ESP_LOGW(TAG, "Error reading AQI data register");
this->status_set_warning();
return;
}
if (this->aqi_ != nullptr) {
// remove reserved bits, just in case they are used in future
data_aqi = ENS160_DATA_AQI & data_aqi;
this->aqi_->publish_state(data_aqi);
}
this->status_clear_warning();
// set temperature and humidity compensation data
this->send_env_data_();
}
void ENS160Component::send_env_data_() {
if (this->temperature_ == nullptr && this->humidity_ == nullptr)
return;
float temperature = NAN;
if (this->temperature_ != nullptr)
temperature = this->temperature_->state;
if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
ESP_LOGW(TAG, "Invalid external temperature - compensation values not updated");
return;
} else {
ESP_LOGV(TAG, "External temperature compensation: %.1f°C", temperature);
}
float humidity = NAN;
if (this->humidity_ != nullptr)
humidity = this->humidity_->state;
if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
ESP_LOGW(TAG, "Invalid external humidity - compensation values not updated");
return;
} else {
ESP_LOGV(TAG, "External humidity compensation: %.1f%%", humidity);
}
uint16_t t = (uint16_t) ((temperature + 273.15f) * 64.0f);
uint16_t h = (uint16_t) (humidity * 512.0f);
uint8_t data[4];
data[0] = t & 0xff;
data[1] = (t >> 8) & 0xff;
data[2] = h & 0xff;
data[3] = (h >> 8) & 0xff;
if (!this->write_bytes(ENS160_REG_TEMP_IN, data, 4)) {
ESP_LOGE(TAG, "Error writing compensation values");
this->status_set_warning();
return;
}
}
void ENS160Component::dump_config() {
ESP_LOGCONFIG(TAG, "ENS160:");
switch (this->error_code_) {
case COMMUNICATION_FAILED:
ESP_LOGE(TAG, "Communication failed! Is the sensor connected?");
break;
case READ_FAILED:
ESP_LOGE(TAG, "Error reading from register");
break;
case WRITE_FAILED:
ESP_LOGE(TAG, "Error writing to register");
break;
case INVALID_ID:
ESP_LOGE(TAG, "Sensor reported an invalid ID. Is this a ENS160?");
break;
case VALIDITY_INVALID:
ESP_LOGE(TAG, "Invalid Device Status - No valid output");
break;
case STD_OPMODE_FAILED:
ESP_LOGE(TAG, "Device failed to achieve Standard Operating Mode");
break;
case NONE:
ESP_LOGD(TAG, "Setup successful");
break;
}
ESP_LOGI(TAG, "Firmware Version: %d.%d.%d", this->firmware_ver_major_, this->firmware_ver_minor_,
this->firmware_ver_build_);
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "CO2 Sensor:", this->co2_);
LOG_SENSOR(" ", "TVOC Sensor:", this->tvoc_);
LOG_SENSOR(" ", "AQI Sensor:", this->aqi_);
if (this->temperature_ != nullptr && this->humidity_ != nullptr) {
LOG_SENSOR(" ", " Temperature Compensation:", this->temperature_);
LOG_SENSOR(" ", " Humidity Compensation:", this->humidity_);
} else {
ESP_LOGCONFIG(TAG, " Compensation: Not configured");
}
}
} // namespace ens160
} // namespace esphome

View File

@ -0,0 +1,60 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace ens160 {
class ENS160Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor {
public:
void set_co2(sensor::Sensor *co2) { co2_ = co2; }
void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; }
void set_aqi(sensor::Sensor *aqi) { aqi_ = aqi; }
void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
void setup() override;
void update() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
void send_env_data_();
enum ErrorCode {
NONE = 0,
COMMUNICATION_FAILED,
INVALID_ID,
VALIDITY_INVALID,
READ_FAILED,
WRITE_FAILED,
STD_OPMODE_FAILED,
} error_code_{NONE};
enum ValidityFlag {
NORMAL_OPERATION = 0,
WARMING_UP,
INITIAL_STARTUP,
INVALID_OUTPUT,
} validity_flag_;
bool warming_up_{false};
bool initial_startup_{false};
uint8_t firmware_ver_major_{0};
uint8_t firmware_ver_minor_{0};
uint8_t firmware_ver_build_{0};
sensor::Sensor *co2_{nullptr};
sensor::Sensor *tvoc_{nullptr};
sensor::Sensor *aqi_{nullptr};
sensor::Sensor *humidity_{nullptr};
sensor::Sensor *temperature_{nullptr};
};
} // namespace ens160
} // namespace esphome

View File

@ -0,0 +1,88 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_ECO2,
CONF_HUMIDITY,
CONF_ID,
CONF_TEMPERATURE,
CONF_TVOC,
DEVICE_CLASS_AQI,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
ICON_CHEMICAL_WEAPON,
ICON_MOLECULE_CO2,
ICON_RADIATOR,
STATE_CLASS_MEASUREMENT,
UNIT_PARTS_PER_BILLION,
UNIT_PARTS_PER_MILLION,
)
CODEOWNERS = ["@vincentscode"]
DEPENDENCIES = ["i2c"]
ens160_ns = cg.esphome_ns.namespace("ens160")
ENS160Component = ens160_ns.class_(
"ENS160Component", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor
)
CONF_AQI = "aqi"
CONF_COMPENSATION = "compensation"
UNIT_INDEX = "index"
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ENS160Component),
cv.Required(CONF_ECO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_AQI): sensor.sensor_schema(
unit_of_measurement=UNIT_INDEX,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_AQI,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_COMPENSATION): cv.Schema(
{
cv.Required(CONF_TEMPERATURE): cv.use_id(sensor.Sensor),
cv.Required(CONF_HUMIDITY): cv.use_id(sensor.Sensor),
}
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x53))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sens = await sensor.new_sensor(config[CONF_ECO2])
cg.add(var.set_co2(sens))
sens = await sensor.new_sensor(config[CONF_TVOC])
cg.add(var.set_tvoc(sens))
sens = await sensor.new_sensor(config[CONF_AQI])
cg.add(var.set_aqi(sens))
if CONF_COMPENSATION in config:
compensation_config = config[CONF_COMPENSATION]
sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE])
cg.add(var.set_temperature(sens))
sens = await cg.get_variable(compensation_config[CONF_HUMIDITY])
cg.add(var.set_humidity(sens))

View File

@ -225,6 +225,13 @@ sensor:
name: "ADE7953 Reactive Power B" name: "ADE7953 Reactive Power B"
update_interval: 1s update_interval: 1s
- platform: ens160
eco2:
name: "ENS160 eCO2"
tvoc:
name: "ENS160 Total Volatile Organic Compounds"
aqi:
name: "ENS160 Air Quality Index"
- platform: tmp102 - platform: tmp102
name: TMP102 Temperature name: TMP102 Temperature
- platform: hm3301 - platform: hm3301
@ -424,7 +431,6 @@ switch:
direction: BACKWARD direction: BACKWARD
id: test_motor id: test_motor
custom_component: custom_component:
lambda: |- lambda: |-
auto s = new CustomComponent(); auto s = new CustomComponent();
@ -613,7 +619,6 @@ mcp23017:
mcp23008: mcp23008:
id: mcp23008_hub id: mcp23008_hub
light: light:
- platform: hbridge - platform: hbridge
name: Icicle Lights name: Icicle Lights
@ -633,7 +638,6 @@ ttp229_bsf:
sdo_pin: D2 sdo_pin: D2
scl_pin: D1 scl_pin: D1
display: display:
- platform: max7219digit - platform: max7219digit
cs_pin: GPIO15 cs_pin: GPIO15
@ -645,7 +649,6 @@ display:
lambda: |- lambda: |-
it.printdigit("hello"); it.printdigit("hello");
http_request: http_request:
useragent: esphome/device useragent: esphome/device
timeout: 10s timeout: 10s

View File

@ -480,7 +480,6 @@ sensor:
name: PZEMDC Power name: PZEMDC Power
energy: energy:
name: PZEMDC Energy name: PZEMDC Energy
- platform: pmsx003 - platform: pmsx003
uart_id: uart_9 uart_id: uart_9
type: PMSX003 type: PMSX003