mirror of
https://github.com/esphome/esphome.git
synced 2026-10-18 04:02:54 +00:00
[sen6x] Add temperature compensation, acceleration and startup delay (#18781)
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
ae62960193
commit
0f6fc503bd
@@ -1,6 +1,7 @@
|
||||
#include "sen6x.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::sen6x {
|
||||
@@ -25,7 +26,6 @@ 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;
|
||||
static constexpr uint16_t SEN6X_CMD_GET_PRODUCT_NAME = 0xD014;
|
||||
@@ -45,6 +45,8 @@ 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_TEMPERATURE_COMPENSATION = 0x60B2;
|
||||
static constexpr uint16_t SEN6X_CMD_TEMPERATURE_ACCELERATION = 0x6100;
|
||||
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;
|
||||
@@ -200,6 +202,16 @@ bool SEN6XComponent::write_setup_step_(SetupStep step) {
|
||||
return false;
|
||||
this->write_tuning_parameters_(SEN6X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value());
|
||||
return true;
|
||||
case SETUP_STEP_TEMPERATURE_COMPENSATION:
|
||||
if (!this->temperature_compensation_.has_value())
|
||||
return false;
|
||||
this->write_temperature_compensation_(this->temperature_compensation_.value());
|
||||
return true;
|
||||
case SETUP_STEP_TEMPERATURE_ACCELERATION:
|
||||
if (!this->temperature_acceleration_.has_value())
|
||||
return false;
|
||||
this->write_temperature_acceleration_(this->temperature_acceleration_.value());
|
||||
return true;
|
||||
case SETUP_STEP_CO2_ASC:
|
||||
if (this->co2_sensor_ == nullptr || !this->co2_asc_.has_value())
|
||||
return false;
|
||||
@@ -220,6 +232,18 @@ bool SEN6XComponent::write_setup_step_(SetupStep step) {
|
||||
}
|
||||
}
|
||||
|
||||
bool SEN6XComponent::write_temperature_compensation_(const TemperatureCompensation &compensation) {
|
||||
// Word 4 selects compensation slot 0; other slots are not exposed
|
||||
uint16_t params[4] = {static_cast<uint16_t>(compensation.offset),
|
||||
static_cast<uint16_t>(compensation.normalized_offset_slope), compensation.time_constant, 0};
|
||||
return this->write_config_words_(SEN6X_CMD_TEMPERATURE_COMPENSATION, params, 4);
|
||||
}
|
||||
|
||||
bool SEN6XComponent::write_temperature_acceleration_(const TemperatureAcceleration &acceleration) {
|
||||
uint16_t params[4] = {acceleration.k, acceleration.p, acceleration.t1, acceleration.t2};
|
||||
return this->write_config_words_(SEN6X_CMD_TEMPERATURE_ACCELERATION, params, 4);
|
||||
}
|
||||
|
||||
bool SEN6XComponent::write_setup_register_(uint16_t i2c_command, uint16_t value) {
|
||||
return this->write_config_words_(i2c_command, &value, 1);
|
||||
}
|
||||
@@ -264,10 +288,21 @@ void SEN6XComponent::dump_config() {
|
||||
" Product: %s\n"
|
||||
" Serial: %s\n"
|
||||
" Firmware: %u.%u\n"
|
||||
" Address: 0x%02X",
|
||||
" Address: 0x%02X\n"
|
||||
" Startup delay: %" PRIu32 " ms",
|
||||
this->product_name_.c_str(), this->serial_number_.c_str(), this->firmware_version_major_,
|
||||
this->firmware_version_minor_, this->address_);
|
||||
this->firmware_version_minor_, this->address_, this->startup_delay_ms_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
if (this->temperature_compensation_.has_value()) {
|
||||
const auto &comp = this->temperature_compensation_.value();
|
||||
ESP_LOGCONFIG(TAG, " Temperature compensation: offset=%.2f slope=%.4f time_constant=%us", comp.offset / 200.0f,
|
||||
comp.normalized_offset_slope / 10000.0f, comp.time_constant);
|
||||
}
|
||||
if (this->temperature_acceleration_.has_value()) {
|
||||
const auto &accel = this->temperature_acceleration_.value();
|
||||
ESP_LOGCONFIG(TAG, " Temperature acceleration: K=%.1f P=%.1f T1=%.1f T2=%.1f", accel.k / 10.0f, accel.p / 10.0f,
|
||||
accel.t1 / 10.0f, accel.t2 / 10.0f);
|
||||
}
|
||||
// setup() clears co2_sensor_ once it detects a variant without CO2
|
||||
if (this->co2_sensor_ != nullptr) {
|
||||
if (this->co2_asc_.has_value()) {
|
||||
@@ -557,7 +592,7 @@ void SEN6XComponent::on_measurement_started_() {
|
||||
}
|
||||
// Values need the warm-up period again after every start
|
||||
this->startup_complete_ = false;
|
||||
this->set_timeout(TIMEOUT_STARTUP, STARTUP_DELAY, [this]() { this->startup_complete_ = true; });
|
||||
this->set_timeout(TIMEOUT_STARTUP, this->startup_delay_ms_, [this]() { this->startup_complete_ = true; });
|
||||
}
|
||||
|
||||
void SEN6XComponent::start_measurement() {
|
||||
|
||||
@@ -20,6 +20,21 @@ struct GasTuning {
|
||||
uint16_t gain_factor;
|
||||
};
|
||||
|
||||
// Raw values in the device's fixed-point encoding (offset x200, slope x10000)
|
||||
struct TemperatureCompensation {
|
||||
int16_t offset;
|
||||
int16_t normalized_offset_slope;
|
||||
uint16_t time_constant;
|
||||
};
|
||||
|
||||
// Raw values in the device's fixed-point encoding (all x10)
|
||||
struct TemperatureAcceleration {
|
||||
uint16_t k;
|
||||
uint16_t p;
|
||||
uint16_t t1;
|
||||
uint16_t t2;
|
||||
};
|
||||
|
||||
// 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;
|
||||
@@ -62,6 +77,13 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
|
||||
NOX_STD_INITIAL,
|
||||
gain_factor};
|
||||
}
|
||||
void set_startup_delay(uint32_t delay_ms) { this->startup_delay_ms_ = delay_ms; }
|
||||
void set_temperature_compensation(int16_t offset, int16_t normalized_offset_slope, uint16_t time_constant) {
|
||||
this->temperature_compensation_ = TemperatureCompensation{offset, normalized_offset_slope, time_constant};
|
||||
}
|
||||
void set_temperature_acceleration(uint16_t k, uint16_t p, uint16_t t1, uint16_t t2) {
|
||||
this->temperature_acceleration_ = TemperatureAcceleration{k, p, t1, t2};
|
||||
}
|
||||
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; }
|
||||
@@ -75,6 +97,8 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
|
||||
enum SetupStep : uint8_t {
|
||||
SETUP_STEP_VOC_TUNING,
|
||||
SETUP_STEP_NOX_TUNING,
|
||||
SETUP_STEP_TEMPERATURE_COMPENSATION,
|
||||
SETUP_STEP_TEMPERATURE_ACCELERATION,
|
||||
SETUP_STEP_CO2_ASC,
|
||||
SETUP_STEP_ALTITUDE,
|
||||
SETUP_STEP_AMBIENT_PRESSURE,
|
||||
@@ -88,6 +112,8 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
|
||||
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);
|
||||
bool write_temperature_compensation_(const TemperatureCompensation &compensation);
|
||||
bool write_temperature_acceleration_(const TemperatureAcceleration &acceleration);
|
||||
void write_ambient_pressure_(uint16_t pressure_hpa);
|
||||
void update_ambient_pressure_compensation_(float pressure_hpa);
|
||||
uint32_t command_wait_remaining_() const;
|
||||
@@ -102,14 +128,17 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
|
||||
std::string serial_number_;
|
||||
optional<GasTuning> voc_tuning_params_;
|
||||
optional<GasTuning> nox_tuning_params_;
|
||||
optional<TemperatureAcceleration> temperature_acceleration_;
|
||||
optional<TemperatureCompensation> temperature_compensation_;
|
||||
uint16_t read_cmd_{0};
|
||||
sensor::Sensor *ambient_pressure_source_{nullptr};
|
||||
uint32_t startup_delay_ms_{60000};
|
||||
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};
|
||||
uint32_t command_wait_{0};
|
||||
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
|
||||
|
||||
@@ -19,13 +19,18 @@ from esphome.const import (
|
||||
CONF_INDEX_OFFSET,
|
||||
CONF_LEARNING_TIME_GAIN_HOURS,
|
||||
CONF_LEARNING_TIME_OFFSET_HOURS,
|
||||
CONF_NORMALIZED_OFFSET_SLOPE,
|
||||
CONF_NOX,
|
||||
CONF_OFFSET,
|
||||
CONF_PM_1_0,
|
||||
CONF_PM_2_5,
|
||||
CONF_PM_4_0,
|
||||
CONF_PM_10_0,
|
||||
CONF_STARTUP_DELAY,
|
||||
CONF_STD_INITIAL,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TEMPERATURE_COMPENSATION,
|
||||
CONF_TIME_CONSTANT,
|
||||
CONF_TYPE,
|
||||
CONF_VOC,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
@@ -47,6 +52,17 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONF_TEMPERATURE_ACCELERATION = "temperature_acceleration"
|
||||
CONF_K = "k"
|
||||
CONF_P = "p"
|
||||
CONF_T1 = "t1"
|
||||
CONF_T2 = "t2"
|
||||
|
||||
# The device takes these as fixed-point words: offset x200, slope x10000, acceleration x10
|
||||
TEMPERATURE_OFFSET_SCALE = 200
|
||||
TEMPERATURE_SLOPE_SCALE = 10000
|
||||
TEMPERATURE_ACCELERATION_SCALE = 10
|
||||
|
||||
CODEOWNERS = ["@martgras", "@mebner86", "@tuct"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensirion_common"]
|
||||
@@ -93,6 +109,8 @@ def _gas_index_schema(
|
||||
).extend({cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema(tuning_schema)})
|
||||
|
||||
|
||||
_acceleration_parameter = cv.float_range(min=0.1, max=6553.5)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.rename_key(CONF_VOC, CONF_VOC_INDEX, removed_in="2027.2.0", component="sen6x"),
|
||||
cv.rename_key(CONF_NOX, CONF_NOX_INDEX, removed_in="2027.2.0", component="sen6x"),
|
||||
@@ -182,6 +200,31 @@ CONFIG_SCHEMA = cv.All(
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_STARTUP_DELAY, default="60s"): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=cv.TimePeriod(hours=1)),
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE_COMPENSATION): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_OFFSET, default=0): cv.float_range(
|
||||
min=-163.84, max=163.835
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_NORMALIZED_OFFSET_SLOPE, default=0
|
||||
): cv.float_range(min=-3.2768, max=3.2767),
|
||||
cv.Optional(CONF_TIME_CONSTANT, default=0): cv.int_range(
|
||||
min=0, max=65535
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE_ACCELERATION): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_K, default=20.0): _acceleration_parameter,
|
||||
cv.Optional(CONF_P, default=20.0): _acceleration_parameter,
|
||||
cv.Optional(CONF_T1, default=100.0): _acceleration_parameter,
|
||||
cv.Optional(CONF_T2, default=300.0): _acceleration_parameter,
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
@@ -208,6 +251,26 @@ async def to_code(config: ConfigType) -> None:
|
||||
if CONF_TYPE in config:
|
||||
cg.add(var.set_type(config[CONF_TYPE]))
|
||||
|
||||
cg.add(var.set_startup_delay(config[CONF_STARTUP_DELAY]))
|
||||
|
||||
if (comp := config.get(CONF_TEMPERATURE_COMPENSATION)) is not None:
|
||||
cg.add(
|
||||
var.set_temperature_compensation(
|
||||
round(comp[CONF_OFFSET] * TEMPERATURE_OFFSET_SCALE),
|
||||
round(comp[CONF_NORMALIZED_OFFSET_SLOPE] * TEMPERATURE_SLOPE_SCALE),
|
||||
comp[CONF_TIME_CONSTANT],
|
||||
)
|
||||
)
|
||||
if (accel := config.get(CONF_TEMPERATURE_ACCELERATION)) is not None:
|
||||
cg.add(
|
||||
var.set_temperature_acceleration(
|
||||
*(
|
||||
round(accel[key] * TEMPERATURE_ACCELERATION_SCALE)
|
||||
for key in (CONF_K, CONF_P, CONF_T1, CONF_T2)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
for key, func_name in SENSOR_MAP.items():
|
||||
if cfg := config.get(key):
|
||||
sens = await sensor.new_sensor(cfg)
|
||||
|
||||
@@ -8,6 +8,16 @@ sensor:
|
||||
id: sen6x_sensor
|
||||
type: SEN69C
|
||||
i2c_id: i2c_bus
|
||||
startup_delay: 30s
|
||||
temperature_compensation:
|
||||
offset: -2.5
|
||||
normalized_offset_slope: 0.01
|
||||
time_constant: 60
|
||||
temperature_acceleration:
|
||||
k: 25.0
|
||||
p: 15.0
|
||||
t1: 120.0
|
||||
t2: 240.0
|
||||
temperature:
|
||||
name: Temperature
|
||||
accuracy_decimals: 1
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Config-only: partial algorithm_tuning blocks, so the schema defaults fill in the
|
||||
# keys that are left out.
|
||||
# Config-only: partial algorithm_tuning, temperature_compensation and
|
||||
# temperature_acceleration blocks, so the schema defaults fill in the keys that are left out.
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
@@ -8,6 +8,10 @@ sensor:
|
||||
id: sen6x_partial_tuning
|
||||
type: SEN65
|
||||
i2c_id: i2c_bus
|
||||
temperature_compensation:
|
||||
offset: 1.5
|
||||
temperature_acceleration:
|
||||
k: 30
|
||||
voc_index:
|
||||
name: VOC Index
|
||||
algorithm_tuning:
|
||||
|
||||
Reference in New Issue
Block a user