mirror of
https://github.com/esphome/esphome.git
synced 2026-10-03 17:55:57 +00:00
[stcc4] Add component for the Sensirion STCC4 CO2 sensor (#19434)
Co-authored-by: will <will-tm@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
will
J. Nick Koston
parent
2d37c6f58b
commit
4075c15bea
@@ -541,6 +541,7 @@ esphome/components/st7735/* @SenexCrenshaw
|
||||
esphome/components/st7789v/* @kbx81
|
||||
esphome/components/st7920/* @marsjan155
|
||||
esphome/components/statsd/* @Links2004
|
||||
esphome/components/stcc4/* @j9brown
|
||||
esphome/components/stts22h/* @B48D81EFCC
|
||||
esphome/components/substitutions/* @esphome/core
|
||||
esphome/components/sun/* @OttoWinter
|
||||
|
||||
@@ -65,14 +65,15 @@ bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len
|
||||
}
|
||||
|
||||
bool SensirionI2CDevice::get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, const uint8_t len,
|
||||
const uint8_t delay_ms) {
|
||||
const uint8_t delay_ms, uint8_t sensirion_options) {
|
||||
if (!this->write_command_(reg, command_len, nullptr, 0)) {
|
||||
ESP_LOGE(TAG, "Write failed: reg=0x%X (%d) err=%d,", reg, command_len, this->last_error_);
|
||||
return false;
|
||||
}
|
||||
delay(delay_ms);
|
||||
bool result = this->read_data(data, len);
|
||||
if (!result) {
|
||||
if (!result &&
|
||||
(this->last_error_ != i2c::ERROR_NOT_ACKNOWLEDGED || !(sensirion_options & SENSIRION_OPTION_READ_MAY_NACK))) {
|
||||
ESP_LOGE(TAG, "Read failed: reg=0x%X err=%d,", reg, this->last_error_);
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -16,6 +16,9 @@ namespace esphome::sensirion_common {
|
||||
*/
|
||||
static const uint8_t CRC_POLYNOMIAL = 0x31; // default for Sensirion
|
||||
|
||||
/// When reading a register and the device reports NACK because the value is not ready yet, don't log it as an error.
|
||||
static constexpr uint8_t SENSIRION_OPTION_READ_MAY_NACK = 1u << 0;
|
||||
|
||||
class SensirionI2CDevice : public i2c::I2CDevice {
|
||||
public:
|
||||
enum CommandLen : uint8_t { ADDR_8_BIT = 1, ADDR_16_BIT = 2 };
|
||||
@@ -139,9 +142,11 @@ class SensirionI2CDevice : public i2c::I2CDevice {
|
||||
* @param data pointer to raw result
|
||||
* @param len number of words to read
|
||||
* @param delay milliseconds to to wait between sending the I2C command and reading the result
|
||||
* @param sensirion_options options for the request
|
||||
* @return true if reading succeeded
|
||||
*/
|
||||
bool get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, uint8_t len, uint8_t delay);
|
||||
bool get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, uint8_t len, uint8_t delay,
|
||||
uint8_t sensirion_options = 0);
|
||||
|
||||
/** last error code from I2C operation
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensirion_common, sensor
|
||||
from esphome.components.const import CONF_HUMIDITY_SOURCE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AMBIENT_PRESSURE_COMPENSATION,
|
||||
CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE,
|
||||
CONF_CO2,
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_MEASUREMENT_MODE,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TEMPERATURE_SOURCE,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ICON_MOLECULE_CO2,
|
||||
ICON_THERMOMETER,
|
||||
ICON_WATER_PERCENT,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@j9brown"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensirion_common"]
|
||||
|
||||
stcc4_ns = cg.esphome_ns.namespace("stcc4")
|
||||
STCC4Component = stcc4_ns.class_(
|
||||
"STCC4Component", cg.PollingComponent, sensirion_common.SensirionI2CDevice
|
||||
)
|
||||
|
||||
MeasurementMode = stcc4_ns.enum("MeasurementMode", is_class=True)
|
||||
|
||||
MEASUREMENT_MODE_OPTIONS = {
|
||||
"continuous": MeasurementMode.CONTINUOUS,
|
||||
"single_shot": MeasurementMode.SINGLE_SHOT,
|
||||
}
|
||||
|
||||
|
||||
def validate_config(config: ConfigType) -> ConfigType:
|
||||
if config[CONF_MEASUREMENT_MODE] == "continuous":
|
||||
if CONF_UPDATE_INTERVAL in config:
|
||||
raise cv.Invalid(
|
||||
"update_interval must not be specified in continuous measurement mode"
|
||||
)
|
||||
elif CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = cv.update_interval("60s")
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(STCC4Component),
|
||||
cv.Optional(CONF_CO2): 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.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
icon=ICON_WATER_PERCENT,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Inclusive(CONF_TEMPERATURE_SOURCE, "rht_compensation"): cv.use_id(
|
||||
sensor.Sensor
|
||||
),
|
||||
cv.Inclusive(CONF_HUMIDITY_SOURCE, "rht_compensation"): cv.use_id(
|
||||
sensor.Sensor
|
||||
),
|
||||
cv.Exclusive(
|
||||
CONF_AMBIENT_PRESSURE_COMPENSATION, "ambient_pressure_compensation"
|
||||
): cv.All(cv.pressure, cv.float_range(min=0.4, max=1.1)),
|
||||
cv.Exclusive(
|
||||
CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE,
|
||||
"ambient_pressure_compensation",
|
||||
): cv.use_id(sensor.Sensor),
|
||||
cv.Optional(CONF_MEASUREMENT_MODE, default="continuous"): cv.enum(
|
||||
MEASUREMENT_MODE_OPTIONS, lower=True
|
||||
),
|
||||
cv.Optional(CONF_UPDATE_INTERVAL): cv.update_interval,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x64))
|
||||
.add_extra(validate_config)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_CO2, var.set_co2_sensor)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
|
||||
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
|
||||
|
||||
if (temperature_source := config.get(CONF_TEMPERATURE_SOURCE)) is not None:
|
||||
sens = await cg.get_variable(temperature_source)
|
||||
cg.add(var.set_temperature_source(sens))
|
||||
|
||||
if (humidity_source := config.get(CONF_HUMIDITY_SOURCE)) is not None:
|
||||
sens = await cg.get_variable(humidity_source)
|
||||
cg.add(var.set_humidity_source(sens))
|
||||
|
||||
if (
|
||||
ambient_pressure_compensation := config.get(CONF_AMBIENT_PRESSURE_COMPENSATION)
|
||||
) is not None:
|
||||
cg.add(
|
||||
var.set_ambient_pressure_compensation(
|
||||
ambient_pressure_compensation * 1000 # convert bar to hPa
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
ambient_pressure_compensation_source := config.get(
|
||||
CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE
|
||||
)
|
||||
) is not None:
|
||||
sens = await cg.get_variable(ambient_pressure_compensation_source)
|
||||
cg.add(var.set_ambient_pressure_source(sens))
|
||||
|
||||
cg.add(var.set_measurement_mode(config[CONF_MEASUREMENT_MODE]))
|
||||
@@ -0,0 +1,294 @@
|
||||
#include <cstdint>
|
||||
|
||||
#include "stcc4.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::stcc4 {
|
||||
|
||||
static const char *const TAG = "stcc4";
|
||||
|
||||
// I2C Commands
|
||||
static constexpr uint16_t STCC4_CMD_START_CONTINUOUS_MEASUREMENT = 0x218b;
|
||||
static constexpr uint16_t STCC4_CMD_STOP_CONTINUOUS_MEASUREMENT = 0x3f86;
|
||||
static constexpr uint16_t STCC4_CMD_MEASURE_SINGLE_SHOT = 0x219d;
|
||||
static constexpr uint16_t STCC4_CMD_READ_MEASUREMENT = 0xec05;
|
||||
static constexpr uint16_t STCC4_CMD_GET_PRODUCT_ID = 0x365b;
|
||||
static constexpr uint16_t STCC4_CMD_SET_RHT_COMPENSATION = 0xe000;
|
||||
static constexpr uint16_t STCC4_CMD_SET_PRESSURE_COMPENSATION = 0xe016;
|
||||
|
||||
// Exit sleep is an 8-bit command (single byte 0x00), not 16-bit
|
||||
static constexpr uint8_t STCC4_CMD_EXIT_SLEEP_MODE = 0x00;
|
||||
|
||||
static constexpr uint32_t STCC4_PRODUCT_ID = 0x0901018a;
|
||||
|
||||
// Timeout for determining when the device is ready for use, in milliseconds.
|
||||
// While waiting for the previous measurement to finish, the device will NACK all I2C requests
|
||||
// and it can take up to 1200 ms for the operation to complete according to the datasheet.
|
||||
static constexpr uint32_t READY_TIMEOUT_MS = 1200;
|
||||
|
||||
// Poll interval for determining when the device is ready for use, in milliseconds.
|
||||
static constexpr uint32_t READY_POLL_INTERVAL_MS = 100;
|
||||
|
||||
// Convert units the the device's representation according to the datasheet.
|
||||
constexpr uint16_t temperature_in_c_to_ticks(float temperature_in_c) {
|
||||
return uint16_t((std::clamp(temperature_in_c, -45.f, 130.f) + 45.f) * 65535.f / 175.f);
|
||||
}
|
||||
|
||||
constexpr float temperature_in_ticks_to_c(uint16_t temperature_in_ticks) {
|
||||
return temperature_in_ticks * 175.f / 65535.f - 45.f;
|
||||
}
|
||||
|
||||
constexpr uint16_t humidity_in_percent_to_ticks(float humidity_in_percent) {
|
||||
return uint16_t((std::clamp(humidity_in_percent, 0.f, 100.f) + 6.f) * 65535.f / 125.f);
|
||||
}
|
||||
|
||||
constexpr float humidity_in_ticks_to_percent(uint16_t humidity_in_ticks) {
|
||||
return humidity_in_ticks * 125.f / 65535.f - 6.f;
|
||||
}
|
||||
|
||||
constexpr uint16_t pressure_in_hpa_to_pa_2(float pressure_in_hpa) {
|
||||
return uint16_t(std::clamp(pressure_in_hpa, 400.f, 1100.f) * 50.f);
|
||||
}
|
||||
|
||||
constexpr float pressure_in_pa_2_to_hpa(uint16_t pressure_in_pa_2) { return pressure_in_pa_2 / 50.f; }
|
||||
|
||||
void STCC4Component::setup() {
|
||||
this->stop_poller(); // not ready yet
|
||||
|
||||
// Wait 100 ms after power up before attempting to communicate with the sensor
|
||||
this->set_timeout(100, [this]() {
|
||||
// Send exit sleep mode command (8-bit, NACK expected), wait 5 ms to exit sleep
|
||||
this->write_command(STCC4_CMD_EXIT_SLEEP_MODE);
|
||||
this->set_timeout(5, [this]() { this->poll_until_ready_for_setup_or_timeout_(millis()); });
|
||||
});
|
||||
}
|
||||
|
||||
void STCC4Component::poll_until_ready_for_setup_or_timeout_(uint32_t start_time) {
|
||||
// Stop continuous measurements in case they were previously running
|
||||
// The device may NACK this request if it is not ready to communicate yet
|
||||
if (this->write_command(STCC4_CMD_STOP_CONTINUOUS_MEASUREMENT)) {
|
||||
// Read product ID to verify communication (6 words: 2 for product_id + 4 for serial)
|
||||
// The device may NACK this request if it is not ready to communicate yet
|
||||
uint16_t raw_product_id[6];
|
||||
if (this->get_register(STCC4_CMD_GET_PRODUCT_ID, raw_product_id, 6, 1)) {
|
||||
uint32_t product_id = (uint32_t(raw_product_id[0]) << 16) | raw_product_id[1];
|
||||
uint64_t serial_number = (uint64_t(raw_product_id[2]) << 48) | (uint64_t(raw_product_id[3]) << 32) |
|
||||
(uint64_t(raw_product_id[4]) << 16) | raw_product_id[5];
|
||||
ESP_LOGD(TAG, "Product ID: 0x%08" PRIX32 ", Serial: 0x%016" PRIX64, product_id, serial_number);
|
||||
if (product_id != STCC4_PRODUCT_ID) {
|
||||
ESP_LOGE(TAG, "Unsupported product ID");
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set static ambient pressure compensation if configured
|
||||
if (this->ambient_pressure_in_pa_2_ != 0) {
|
||||
if (!this->write_ambient_pressure_compensation_(this->ambient_pressure_in_pa_2_)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the dynamic compensation sources' current values, if configured
|
||||
if (this->temperature_source_ != nullptr && this->humidity_source_ != nullptr) {
|
||||
this->update_rht_compensation_from_source_();
|
||||
}
|
||||
if (this->ambient_pressure_source_ != nullptr) {
|
||||
this->update_ambient_pressure_compensation_from_source_();
|
||||
}
|
||||
|
||||
if (this->measurement_mode_ == MeasurementMode::SINGLE_SHOT) {
|
||||
this->start_poller();
|
||||
this->finish_setup_();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start continuous measurement
|
||||
if (!this->write_command(STCC4_CMD_START_CONTINUOUS_MEASUREMENT)) {
|
||||
ESP_LOGE(TAG, "Failed to start continuous measurement");
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
this->schedule_continuous_update_(false);
|
||||
this->finish_setup_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (millis() - start_time < READY_TIMEOUT_MS) {
|
||||
ESP_LOGVV(TAG, "Retry sync");
|
||||
this->set_timeout(READY_POLL_INTERVAL_MS,
|
||||
[this, start_time]() { this->poll_until_ready_for_setup_or_timeout_(start_time); });
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "Failed to stop continuous measurements and read product ID");
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
}
|
||||
|
||||
void STCC4Component::finish_setup_() {
|
||||
this->ready_ = true;
|
||||
// Follow the sources only once measuring started, so a failed setup stops writing to the device
|
||||
if (this->temperature_source_ != nullptr && this->humidity_source_ != nullptr) {
|
||||
this->temperature_source_->add_on_state_callback([this](float) { this->update_rht_compensation_from_source_(); });
|
||||
this->humidity_source_->add_on_state_callback([this](float) { this->update_rht_compensation_from_source_(); });
|
||||
}
|
||||
if (this->ambient_pressure_source_ != nullptr) {
|
||||
this->ambient_pressure_source_->add_on_state_callback(
|
||||
[this](float) { this->update_ambient_pressure_compensation_from_source_(); });
|
||||
}
|
||||
}
|
||||
|
||||
void STCC4Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "STCC4:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Measurement mode: %s",
|
||||
this->measurement_mode_ == MeasurementMode::CONTINUOUS ? LOG_STR_LITERAL("Continuous (1s)")
|
||||
: LOG_STR_LITERAL("Single shot"));
|
||||
if (this->ambient_pressure_source_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " Dynamic ambient pressure compensation using '%s'",
|
||||
this->ambient_pressure_source_->get_name().c_str());
|
||||
} else if (this->ambient_pressure_in_pa_2_ != 0) {
|
||||
ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %f hPa",
|
||||
pressure_in_pa_2_to_hpa(this->ambient_pressure_in_pa_2_));
|
||||
}
|
||||
if (this->temperature_source_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " Temperature compensation using '%s'", this->temperature_source_->get_name().c_str());
|
||||
}
|
||||
if (this->humidity_source_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " Humidity compensation using '%s'", this->humidity_source_->get_name().c_str());
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
}
|
||||
|
||||
void STCC4Component::update() {
|
||||
if (!this->ready_ || this->measurement_mode_ != MeasurementMode::SINGLE_SHOT)
|
||||
return;
|
||||
|
||||
// Perform single-shot measurement, wait 500 ms for the measurement to be ready
|
||||
if (!this->write_command(STCC4_CMD_MEASURE_SINGLE_SHOT)) {
|
||||
ESP_LOGW(TAG, "Failed to start single shot measurement");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
this->set_timeout(500, [this]() {
|
||||
if (this->read_measurement_(0)) {
|
||||
this->status_clear_warning();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Failed to read measurement data");
|
||||
this->status_set_warning();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void STCC4Component::schedule_continuous_update_(bool retry_for_clock_drift) {
|
||||
// In continuous measurement mode, the STCC4 produces a sample every 1000 ms according to its
|
||||
// internal clock. The datasheet recommends retrying 150 ms after a failed read to compensate
|
||||
// for clock drift between the host and the device.
|
||||
this->set_timeout(retry_for_clock_drift ? 150 : 1000, [this, retry_for_clock_drift]() {
|
||||
if (this->read_measurement_(retry_for_clock_drift ? 0 : sensirion_common::SENSIRION_OPTION_READ_MAY_NACK)) {
|
||||
this->status_clear_warning();
|
||||
this->schedule_continuous_update_(false);
|
||||
} else if (!retry_for_clock_drift) {
|
||||
this->schedule_continuous_update_(true);
|
||||
} else {
|
||||
this->status_set_warning();
|
||||
this->schedule_continuous_update_(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool STCC4Component::read_measurement_(uint8_t sensirion_options) {
|
||||
// Read measurement data: 4 words (CO2, temperature, humidity, status)
|
||||
uint16_t raw_data[4];
|
||||
if (!this->get_register_(STCC4_CMD_READ_MEASUREMENT, ADDR_16_BIT, raw_data, 4, 1, sensirion_options)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// CO2 value is in ppm as int16 (ignore negative values during warm-up)
|
||||
const int16_t co2_raw = int16_t(raw_data[0]);
|
||||
if (this->co2_sensor_ != nullptr && co2_raw >= 0) {
|
||||
this->co2_sensor_->publish_state(co2_raw);
|
||||
}
|
||||
|
||||
if (this->temperature_sensor_ != nullptr) {
|
||||
this->temperature_sensor_->publish_state(temperature_in_ticks_to_c(raw_data[1]));
|
||||
}
|
||||
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
this->humidity_sensor_->publish_state(humidity_in_ticks_to_percent(raw_data[2]));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void STCC4Component::update_rht_compensation_from_source_() {
|
||||
const float temperature_in_c = this->temperature_source_->state;
|
||||
const float humidity_in_percent = this->humidity_source_->state;
|
||||
if (std::isnan(temperature_in_c) || std::isnan(humidity_in_percent))
|
||||
return;
|
||||
|
||||
const uint16_t temperature_in_ticks = temperature_in_c_to_ticks(temperature_in_c);
|
||||
const uint16_t humidity_in_ticks = humidity_in_percent_to_ticks(humidity_in_percent);
|
||||
if ((this->temperature_in_ticks_ != temperature_in_ticks || this->humidity_in_ticks_ != humidity_in_ticks) &&
|
||||
this->write_rht_compensation_(temperature_in_ticks, humidity_in_ticks)) {
|
||||
this->temperature_in_ticks_ = temperature_in_ticks;
|
||||
this->humidity_in_ticks_ = humidity_in_ticks;
|
||||
}
|
||||
}
|
||||
|
||||
void STCC4Component::set_ambient_pressure_compensation(float pressure_in_hpa) {
|
||||
this->ambient_pressure_in_pa_2_ = pressure_in_hpa_to_pa_2(pressure_in_hpa);
|
||||
}
|
||||
|
||||
void STCC4Component::update_ambient_pressure_compensation_from_source_() {
|
||||
const float pressure_in_hpa = this->ambient_pressure_source_->state;
|
||||
if (std::isnan(pressure_in_hpa))
|
||||
return;
|
||||
|
||||
if (pressure_in_hpa < 100.f || pressure_in_hpa > 10000.f) {
|
||||
// Some pressure sensors report values in Pa instead of hPa and there's no way to check at compile time.
|
||||
// Warn if the value seems far outside of the expected range.
|
||||
if (!this->ambient_pressure_unit_warning_logged_) {
|
||||
this->ambient_pressure_unit_warning_logged_ = true;
|
||||
ESP_LOGW(TAG, "Ambient pressure compensation sensor might have incompatible units: got %f hPa", pressure_in_hpa);
|
||||
}
|
||||
return; // skip this update
|
||||
} else {
|
||||
this->ambient_pressure_unit_warning_logged_ = false;
|
||||
}
|
||||
|
||||
const uint16_t ambient_pressure_in_pa_2 = pressure_in_hpa_to_pa_2(pressure_in_hpa);
|
||||
if (this->ambient_pressure_in_pa_2_ != ambient_pressure_in_pa_2 &&
|
||||
this->write_ambient_pressure_compensation_(ambient_pressure_in_pa_2)) {
|
||||
this->ambient_pressure_in_pa_2_ = ambient_pressure_in_pa_2;
|
||||
}
|
||||
}
|
||||
|
||||
bool STCC4Component::write_rht_compensation_(uint16_t temperature_in_ticks, uint16_t humidity_in_ticks) {
|
||||
ESP_LOGVV(TAG, "Set RHT compensation: %f °C, %f %%RH", temperature_in_ticks_to_c(temperature_in_ticks),
|
||||
humidity_in_ticks_to_percent(humidity_in_ticks));
|
||||
const uint16_t data[2] = {temperature_in_ticks, humidity_in_ticks};
|
||||
if (!this->write_command(STCC4_CMD_SET_RHT_COMPENSATION, data, 2)) {
|
||||
ESP_LOGE(TAG, "Failed to set RHT compensation");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool STCC4Component::write_ambient_pressure_compensation_(uint16_t pressure_in_pa_2) {
|
||||
ESP_LOGVV(TAG, "Set pressure compensation: %f hPa", pressure_in_pa_2_to_hpa(pressure_in_pa_2));
|
||||
if (!this->write_command(STCC4_CMD_SET_PRESSURE_COMPENSATION, pressure_in_pa_2)) {
|
||||
ESP_LOGE(TAG, "Failed to set ambient pressure compensation");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::stcc4
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/sensirion_common/i2c_sensirion.h"
|
||||
|
||||
namespace esphome::stcc4 {
|
||||
|
||||
enum class MeasurementMode : uint8_t {
|
||||
CONTINUOUS = 0,
|
||||
SINGLE_SHOT,
|
||||
};
|
||||
|
||||
class STCC4Component final : public PollingComponent, public sensirion_common::SensirionI2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
|
||||
void set_co2_sensor(sensor::Sensor *co2) { this->co2_sensor_ = co2; }
|
||||
void set_temperature_sensor(sensor::Sensor *temperature) { this->temperature_sensor_ = temperature; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity) { this->humidity_sensor_ = humidity; }
|
||||
void set_temperature_source(sensor::Sensor *temperature) { this->temperature_source_ = temperature; }
|
||||
void set_humidity_source(sensor::Sensor *humidity) { this->humidity_source_ = humidity; }
|
||||
void set_ambient_pressure_compensation(float pressure_in_hpa);
|
||||
void set_ambient_pressure_source(sensor::Sensor *pressure) { this->ambient_pressure_source_ = pressure; }
|
||||
void set_measurement_mode(MeasurementMode mode) { this->measurement_mode_ = mode; }
|
||||
|
||||
protected:
|
||||
void poll_until_ready_for_setup_or_timeout_(uint32_t start_time);
|
||||
void finish_setup_();
|
||||
void schedule_continuous_update_(bool retry_for_clock_drift);
|
||||
bool read_measurement_(uint8_t sensirion_options);
|
||||
void update_rht_compensation_from_source_();
|
||||
void update_ambient_pressure_compensation_from_source_();
|
||||
bool write_rht_compensation_(uint16_t temperature_in_ticks, uint16_t humidity_in_ticks);
|
||||
bool write_ambient_pressure_compensation_(uint16_t pressure_in_pa_2);
|
||||
|
||||
sensor::Sensor *co2_sensor_{nullptr};
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
sensor::Sensor *temperature_source_{nullptr};
|
||||
sensor::Sensor *humidity_source_{nullptr};
|
||||
sensor::Sensor *ambient_pressure_source_{nullptr};
|
||||
|
||||
MeasurementMode measurement_mode_{MeasurementMode::CONTINUOUS};
|
||||
|
||||
uint16_t temperature_in_ticks_{0};
|
||||
uint16_t humidity_in_ticks_{0};
|
||||
uint16_t ambient_pressure_in_pa_2_{0};
|
||||
bool ambient_pressure_unit_warning_logged_{false};
|
||||
bool ready_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::stcc4
|
||||
@@ -0,0 +1,36 @@
|
||||
sensor:
|
||||
# SHT4x for external RHT compensation (boards without onboard SHT4x)
|
||||
- platform: sht4x
|
||||
i2c_id: i2c_bus
|
||||
temperature:
|
||||
name: SHT4x Temperature
|
||||
id: sht4x_temperature
|
||||
humidity:
|
||||
name: SHT4x Humidity
|
||||
id: sht4x_humidity
|
||||
- platform: template
|
||||
id: ambient_pressure
|
||||
unit_of_measurement: "hPa"
|
||||
lambda: 'return 1013;'
|
||||
|
||||
# STCC4 CO2 sensor with onboard temperature/humidity and external compensation
|
||||
- platform: stcc4
|
||||
i2c_id: i2c_bus
|
||||
id: stcc4_sensor_single_shot
|
||||
co2:
|
||||
name: STCC4 CO2
|
||||
temperature:
|
||||
name: STCC4 Temperature
|
||||
humidity:
|
||||
name: STCC4 Humidity
|
||||
temperature_source: sht4x_temperature
|
||||
humidity_source: sht4x_humidity
|
||||
measurement_mode: single_shot
|
||||
ambient_pressure_compensation: 1013 mbar
|
||||
update_interval: 60s
|
||||
- platform: stcc4
|
||||
i2c_id: i2c_bus
|
||||
address: 0x65
|
||||
id: stcc4_sensor_continuous
|
||||
measurement_mode: continuous
|
||||
ambient_pressure_compensation_source: ambient_pressure
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
Reference in New Issue
Block a user