mirror of
https://github.com/esphome/esphome.git
synced 2026-10-18 04:02:54 +00:00
[sen6x] Add CO2 configuration options (#18780)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
Jonathan Swoboda
J. Nick Koston
J. Nick Koston
parent
5759c6b8e6
commit
325c747dcd
@@ -22,6 +22,9 @@ static constexpr uint32_t STOP_MEASUREMENT_DELAY = 1400;
|
||||
static constexpr uint32_t FAN_CLEANING_DELAY = 10000;
|
||||
static constexpr uint32_t SHT_HEATER_DELAY = 20000;
|
||||
static constexpr uint32_t CO2_CONDITIONING_DELAY = 24000;
|
||||
// Accepted range of a runtime pressure source, the same as the schema's ambient_pressure_compensation
|
||||
static constexpr float AMBIENT_PRESSURE_MIN_HPA = 700.0f;
|
||||
static constexpr float AMBIENT_PRESSURE_MAX_HPA = 1200.0f;
|
||||
static constexpr uint32_t STARTUP_DELAY = 60000; // ms before published values are considered valid
|
||||
static constexpr uint16_t SEN6X_CMD_GET_DATA_READY_STATUS = 0x0202;
|
||||
static constexpr uint16_t SEN6X_CMD_GET_FIRMWARE_VERSION = 0xD100;
|
||||
@@ -42,6 +45,9 @@ static constexpr uint16_t SEN6X_CMD_ACTIVATE_SHT_HEATER = 0x6765;
|
||||
static constexpr uint16_t SEN6X_CMD_RESET = 0xD304;
|
||||
static constexpr uint16_t SEN6X_CMD_VOC_ALGORITHM_TUNING = 0x60D0;
|
||||
static constexpr uint16_t SEN6X_CMD_NOX_ALGORITHM_TUNING = 0x60E1;
|
||||
static constexpr uint16_t SEN6X_CMD_CO2_AUTOMATIC_SELF_CAL = 0x6711;
|
||||
static constexpr uint16_t SEN6X_CMD_AMBIENT_PRESSURE = 0x6720;
|
||||
static constexpr uint16_t SEN6X_CMD_SENSOR_ALTITUDE = 0x6736;
|
||||
|
||||
static inline void set_read_command_and_words(SEN6XComponent::Sen6xType type, uint16_t &read_cmd, uint8_t &read_words) {
|
||||
read_cmd = SEN6X_CMD_READ_MEASUREMENT;
|
||||
@@ -168,32 +174,54 @@ void SEN6XComponent::setup() {
|
||||
});
|
||||
}
|
||||
|
||||
// One configuration write per invocation, spaced by CMD_EXEC_DELAY. Cases without a
|
||||
// configured value fall through; each taken case must advance setup_step_index_ so the
|
||||
// next invocation resumes at the following step. These writes are optional, so a failure
|
||||
// only warns and the chain continues to the mandatory start-measurements write.
|
||||
// One configuration write per invocation, spaced by CMD_EXEC_DELAY, then the start command.
|
||||
// These writes are optional, so a failure only warns and the chain carries on.
|
||||
void SEN6XComponent::run_next_setup_step_() {
|
||||
switch (this->setup_step_index_) {
|
||||
// Tuning writes are skipped when setup() disabled the sensor for this variant
|
||||
case 0:
|
||||
this->setup_step_index_++;
|
||||
if (this->voc_sensor_ != nullptr && this->voc_tuning_params_.has_value()) {
|
||||
this->write_tuning_parameters_(SEN6X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value());
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
case 1:
|
||||
this->setup_step_index_++;
|
||||
if (this->nox_sensor_ != nullptr && this->nox_tuning_params_.has_value()) {
|
||||
this->write_tuning_parameters_(SEN6X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value());
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
default:
|
||||
this->finish_setup_();
|
||||
while (this->setup_step_index_ < SETUP_STEP_COUNT) {
|
||||
if (this->write_setup_step_(static_cast<SetupStep>(this->setup_step_index_++))) {
|
||||
this->set_timeout(TIMEOUT_SETUP_STEP, CMD_EXEC_DELAY, [this]() { this->run_next_setup_step_(); });
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->set_timeout(TIMEOUT_SETUP_STEP, CMD_EXEC_DELAY, [this]() { this->run_next_setup_step_(); });
|
||||
this->finish_setup_();
|
||||
}
|
||||
|
||||
// Returns false when the step has nothing to write: the setting is not configured, or setup()
|
||||
// disabled its sensor for this variant
|
||||
bool SEN6XComponent::write_setup_step_(SetupStep step) {
|
||||
switch (step) {
|
||||
case SETUP_STEP_VOC_TUNING:
|
||||
if (this->voc_sensor_ == nullptr || !this->voc_tuning_params_.has_value())
|
||||
return false;
|
||||
this->write_tuning_parameters_(SEN6X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value());
|
||||
return true;
|
||||
case SETUP_STEP_NOX_TUNING:
|
||||
if (this->nox_sensor_ == nullptr || !this->nox_tuning_params_.has_value())
|
||||
return false;
|
||||
this->write_tuning_parameters_(SEN6X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value());
|
||||
return true;
|
||||
case SETUP_STEP_CO2_ASC:
|
||||
if (this->co2_sensor_ == nullptr || !this->co2_asc_.has_value())
|
||||
return false;
|
||||
this->write_setup_register_(SEN6X_CMD_CO2_AUTOMATIC_SELF_CAL, this->co2_asc_.value() ? 1 : 0);
|
||||
return true;
|
||||
case SETUP_STEP_ALTITUDE:
|
||||
if (this->co2_sensor_ == nullptr || this->altitude_compensation_ == ALTITUDE_UNSET)
|
||||
return false;
|
||||
this->write_setup_register_(SEN6X_CMD_SENSOR_ALTITUDE, this->altitude_compensation_);
|
||||
return true;
|
||||
case SETUP_STEP_AMBIENT_PRESSURE:
|
||||
if (this->co2_sensor_ == nullptr || this->ambient_pressure_ == PRESSURE_UNSET)
|
||||
return false;
|
||||
this->write_ambient_pressure_(this->ambient_pressure_);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool SEN6XComponent::write_setup_register_(uint16_t i2c_command, uint16_t value) {
|
||||
return this->write_config_words_(i2c_command, &value, 1);
|
||||
}
|
||||
|
||||
void SEN6XComponent::finish_setup_() {
|
||||
@@ -208,14 +236,15 @@ void SEN6XComponent::finish_setup_() {
|
||||
ESP_LOGD(TAG, "Initialized");
|
||||
}
|
||||
|
||||
// Writes one optional configuration command. A failure warns and returns false, but does
|
||||
// not stop setup: the sensor still measures with that setting left at its default.
|
||||
// Writes one optional configuration command and reserves its execution time. A failure warns and
|
||||
// returns false, but does not stop setup: the sensor still measures with that setting left at its default.
|
||||
bool SEN6XComponent::write_config_words_(uint16_t i2c_command, const uint16_t *data, uint8_t len) {
|
||||
if (!this->write_command(i2c_command, data, len)) {
|
||||
ESP_LOGE(TAG, "Write 0x%04X failed, error %d", i2c_command, this->last_error_);
|
||||
this->status_set_warning();
|
||||
return false;
|
||||
}
|
||||
this->set_command_wait_(CMD_EXEC_DELAY);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -239,6 +268,22 @@ void SEN6XComponent::dump_config() {
|
||||
this->product_name_.c_str(), this->serial_number_.c_str(), this->firmware_version_major_,
|
||||
this->firmware_version_minor_, this->address_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
// setup() clears co2_sensor_ once it detects a variant without CO2
|
||||
if (this->co2_sensor_ != nullptr) {
|
||||
if (this->co2_asc_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " CO2 automatic self-calibration: %s", ONOFF(this->co2_asc_.value()));
|
||||
}
|
||||
if (this->altitude_compensation_ != ALTITUDE_UNSET) {
|
||||
ESP_LOGCONFIG(TAG, " Altitude compensation: %u m", this->altitude_compensation_);
|
||||
}
|
||||
if (this->ambient_pressure_ != PRESSURE_UNSET) {
|
||||
ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %u hPa", this->ambient_pressure_);
|
||||
}
|
||||
if (this->ambient_pressure_source_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " Ambient pressure compensation source: %s",
|
||||
this->ambient_pressure_source_->get_name().c_str());
|
||||
}
|
||||
}
|
||||
LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_);
|
||||
LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_);
|
||||
LOG_SENSOR(" ", "PM 4.0", this->pm_4_0_sensor_);
|
||||
@@ -255,9 +300,17 @@ void SEN6XComponent::update() {
|
||||
if (!this->initialized_ || !this->measuring_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel any in-flight polling from a previous update() cycle.
|
||||
// Cancel any in-flight polling from a previous update() cycle before touching the bus.
|
||||
this->cancel_timeout(TIMEOUT_POLL);
|
||||
// A command is still executing (an action moments ago): run this cycle once its window has passed
|
||||
if (const uint32_t wait = this->command_wait_remaining_(); wait != 0) {
|
||||
this->set_timeout(TIMEOUT_POLL, wait, [this]() { this->update(); });
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->ambient_pressure_source_ != nullptr && this->co2_sensor_ != nullptr) {
|
||||
this->update_ambient_pressure_compensation_(this->ambient_pressure_source_->state);
|
||||
}
|
||||
|
||||
set_read_command_and_words(this->sen6x_type_, this->read_cmd_, this->read_words_);
|
||||
|
||||
@@ -286,6 +339,11 @@ void SEN6XComponent::poll_data_ready_() {
|
||||
ESP_LOGD(TAG, "Data not ready");
|
||||
return;
|
||||
}
|
||||
// A set command (the pressure write above all) gets its execution time before the chain writes again
|
||||
if (const uint32_t wait = this->command_wait_remaining_(); wait != 0) {
|
||||
this->set_timeout(TIMEOUT_POLL, wait, [this]() { this->poll_data_ready_(); });
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, "Data ready polling attempt %u",
|
||||
static_cast<unsigned>(POLL_RETRIES - this->poll_retries_remaining_ + 1));
|
||||
this->poll_retries_remaining_--;
|
||||
@@ -454,8 +512,35 @@ void SEN6XComponent::parse_and_publish_measurements_() {
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
// True while the previous command's execution or settling time has not elapsed
|
||||
bool SEN6XComponent::command_blocked_() const { return millis() - this->command_started_at_ < this->command_wait_; }
|
||||
void SEN6XComponent::write_ambient_pressure_(uint16_t pressure_hpa) {
|
||||
if (this->write_setup_register_(SEN6X_CMD_AMBIENT_PRESSURE, pressure_hpa))
|
||||
this->last_ambient_pressure_ = pressure_hpa;
|
||||
}
|
||||
|
||||
void SEN6XComponent::update_ambient_pressure_compensation_(float pressure_hpa) {
|
||||
if (std::isnan(pressure_hpa))
|
||||
return;
|
||||
// Range-check before narrowing so out-of-unit sources (e.g. Pa) can't wrap into range
|
||||
if (pressure_hpa < AMBIENT_PRESSURE_MIN_HPA || pressure_hpa > AMBIENT_PRESSURE_MAX_HPA) {
|
||||
if (!this->pressure_range_warned_) {
|
||||
ESP_LOGW(TAG, "Ambient pressure out of range: %.0f hPa", pressure_hpa);
|
||||
this->pressure_range_warned_ = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this->pressure_range_warned_ = false;
|
||||
const auto value = static_cast<uint16_t>(lroundf(pressure_hpa));
|
||||
if (value != this->last_ambient_pressure_)
|
||||
this->write_ambient_pressure_(value);
|
||||
}
|
||||
|
||||
// Time left in the previous command's execution or settling window, 0 once it has passed
|
||||
uint32_t SEN6XComponent::command_wait_remaining_() const {
|
||||
const uint32_t elapsed = millis() - this->command_started_at_;
|
||||
return elapsed < this->command_wait_ ? this->command_wait_ - elapsed : 0;
|
||||
}
|
||||
|
||||
bool SEN6XComponent::command_blocked_() const { return this->command_wait_remaining_() != 0; }
|
||||
|
||||
void SEN6XComponent::set_command_wait_(uint32_t wait_ms) {
|
||||
this->command_started_at_ = millis();
|
||||
|
||||
@@ -20,6 +20,10 @@ struct GasTuning {
|
||||
uint16_t gain_factor;
|
||||
};
|
||||
|
||||
// The CO2 settings a config may leave out: altitude is 0 to 3000 m, pressure 700 to 1200 hPa
|
||||
inline constexpr uint16_t ALTITUDE_UNSET = 0xFFFF;
|
||||
inline constexpr uint16_t PRESSURE_UNSET = 0;
|
||||
|
||||
class SEN6XComponent final : public PollingComponent, public sensirion_common::SensirionI2CDevice {
|
||||
SUB_SENSOR(pm_1_0)
|
||||
SUB_SENSOR(pm_2_5)
|
||||
@@ -58,17 +62,35 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
|
||||
NOX_STD_INITIAL,
|
||||
gain_factor};
|
||||
}
|
||||
void set_automatic_self_calibration(bool enabled) { this->co2_asc_ = enabled; }
|
||||
void set_altitude_compensation(uint16_t altitude) { this->altitude_compensation_ = altitude; }
|
||||
void set_ambient_pressure_compensation(uint16_t pressure_hpa) { this->ambient_pressure_ = pressure_hpa; }
|
||||
void set_ambient_pressure_source(sensor::Sensor *pressure) { this->ambient_pressure_source_ = pressure; }
|
||||
void start_measurement();
|
||||
void stop_measurement();
|
||||
void start_fan_cleaning();
|
||||
void activate_sht_heater();
|
||||
|
||||
protected:
|
||||
enum SetupStep : uint8_t {
|
||||
SETUP_STEP_VOC_TUNING,
|
||||
SETUP_STEP_NOX_TUNING,
|
||||
SETUP_STEP_CO2_ASC,
|
||||
SETUP_STEP_ALTITUDE,
|
||||
SETUP_STEP_AMBIENT_PRESSURE,
|
||||
SETUP_STEP_COUNT,
|
||||
};
|
||||
|
||||
Sen6xType infer_type_from_product_name_(const std::string &product_name);
|
||||
void run_next_setup_step_();
|
||||
bool write_setup_step_(SetupStep step);
|
||||
void finish_setup_();
|
||||
bool write_config_words_(uint16_t i2c_command, const uint16_t *data, uint8_t len);
|
||||
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning);
|
||||
bool write_setup_register_(uint16_t i2c_command, uint16_t value);
|
||||
void write_ambient_pressure_(uint16_t pressure_hpa);
|
||||
void update_ambient_pressure_compensation_(float pressure_hpa);
|
||||
uint32_t command_wait_remaining_() const;
|
||||
bool command_blocked_() const;
|
||||
void set_command_wait_(uint32_t wait_ms);
|
||||
void on_measurement_started_();
|
||||
@@ -80,6 +102,7 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
|
||||
std::string serial_number_;
|
||||
optional<GasTuning> voc_tuning_params_;
|
||||
optional<GasTuning> nox_tuning_params_;
|
||||
sensor::Sensor *ambient_pressure_source_{nullptr};
|
||||
Sen6xType sen6x_type_{UNKNOWN};
|
||||
// Post-command wait windows, stored as start time plus duration so elapsed-time checks survive millis() wrap
|
||||
uint32_t command_started_at_{0};
|
||||
@@ -87,12 +110,18 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
|
||||
uint32_t co2_started_at_{0};
|
||||
uint32_t co2_wait_{0};
|
||||
uint16_t read_cmd_{0};
|
||||
uint16_t altitude_compensation_{ALTITUDE_UNSET};
|
||||
uint16_t ambient_pressure_{PRESSURE_UNSET};
|
||||
// Last pressure written to the device, used to skip redundant writes
|
||||
uint16_t last_ambient_pressure_{PRESSURE_UNSET};
|
||||
optional<bool> co2_asc_;
|
||||
uint8_t setup_step_index_{0};
|
||||
uint8_t firmware_version_major_{0};
|
||||
uint8_t firmware_version_minor_{0};
|
||||
uint8_t poll_retries_remaining_{0};
|
||||
uint8_t read_words_{0};
|
||||
bool initialized_{false};
|
||||
bool pressure_range_warned_{false};
|
||||
bool measuring_{false};
|
||||
bool startup_complete_{false};
|
||||
};
|
||||
|
||||
@@ -6,6 +6,10 @@ from esphome.components.const import CONF_NOX_INDEX, CONF_VOC_INDEX
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ALGORITHM_TUNING,
|
||||
CONF_ALTITUDE_COMPENSATION,
|
||||
CONF_AMBIENT_PRESSURE_COMPENSATION,
|
||||
CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE,
|
||||
CONF_AUTOMATIC_SELF_CALIBRATION,
|
||||
CONF_CO2,
|
||||
CONF_FORMALDEHYDE,
|
||||
CONF_GAIN_FACTOR,
|
||||
@@ -155,6 +159,22 @@ CONFIG_SCHEMA = cv.All(
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_AUTOMATIC_SELF_CALIBRATION): cv.boolean,
|
||||
cv.Optional(CONF_ALTITUDE_COMPENSATION): cv.All(
|
||||
cv.float_with_unit(
|
||||
"altitude", "(m|m a.s.l.|MAMSL|MASL)", optional_unit=True
|
||||
),
|
||||
cv.int_range(min=0, max=3000),
|
||||
),
|
||||
cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION): cv.int_range(
|
||||
min=700, max=1200
|
||||
),
|
||||
cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE): cv.use_id(
|
||||
sensor.Sensor
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema(
|
||||
unit_of_measurement="ppb",
|
||||
@@ -209,6 +229,18 @@ async def to_code(config: ConfigType) -> None:
|
||||
args.append(std_initial)
|
||||
args.append(tuning[CONF_GAIN_FACTOR])
|
||||
cg.add(getattr(var, setter)(*args))
|
||||
if co2_cfg := config.get(CONF_CO2):
|
||||
if (asc := co2_cfg.get(CONF_AUTOMATIC_SELF_CALIBRATION)) is not None:
|
||||
cg.add(var.set_automatic_self_calibration(asc))
|
||||
if (altitude := co2_cfg.get(CONF_ALTITUDE_COMPENSATION)) is not None:
|
||||
cg.add(var.set_altitude_compensation(altitude))
|
||||
if (pressure := co2_cfg.get(CONF_AMBIENT_PRESSURE_COMPENSATION)) is not None:
|
||||
cg.add(var.set_ambient_pressure_compensation(pressure))
|
||||
if (
|
||||
source := co2_cfg.get(CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE)
|
||||
) is not None:
|
||||
sens = await cg.get_variable(source)
|
||||
cg.add(var.set_ambient_pressure_source(sens))
|
||||
|
||||
|
||||
SEN6X_ACTION_SCHEMA = maybe_simple_id(
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
sensor:
|
||||
- platform: template
|
||||
id: pressure_source
|
||||
name: Pressure Source
|
||||
unit_of_measurement: hPa
|
||||
# Test with explicit type parameter
|
||||
- platform: sen6x
|
||||
id: sen6x_sensor
|
||||
@@ -45,6 +49,10 @@ sensor:
|
||||
gain_factor: 180
|
||||
co2:
|
||||
name: Carbon Dioxide
|
||||
automatic_self_calibration: true
|
||||
altitude_compensation: 100
|
||||
ambient_pressure_compensation: 1013
|
||||
ambient_pressure_compensation_source: pressure_source
|
||||
formaldehyde:
|
||||
name: Formaldehyde
|
||||
address: 0x6B
|
||||
|
||||
Reference in New Issue
Block a user