From d285706b41d30be068cbf87782f76810cb36a20b Mon Sep 17 00:00:00 2001 From: Big Mike Date: Fri, 23 Jan 2026 17:03:23 -0600 Subject: [PATCH] [sen5x] Fix store baseline functionality (#13469) --- esphome/components/sen5x/sen5x.cpp | 94 +++++++++++++----------------- esphome/components/sen5x/sen5x.h | 15 ++--- esphome/components/sen5x/sensor.py | 1 + 3 files changed, 45 insertions(+), 65 deletions(-) diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index d5c9dfa3ae..09d93a2b2f 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -124,8 +124,8 @@ void SEN5XComponent::setup() { sen5x_type = SEN55; } } - ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str()); } + ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str()); if (this->humidity_sensor_ && sen5x_type == SEN50) { ESP_LOGE(TAG, "Relative humidity requires a SEN54 or SEN55"); this->humidity_sensor_ = nullptr; // mark as not used @@ -159,28 +159,14 @@ void SEN5XComponent::setup() { // This ensures the baseline storage is cleared after OTA // Serial numbers are unique to each sensor, so multiple sensors can be used without conflict uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), combined_serial); - this->pref_ = global_preferences->make_preference(hash, true); - - if (this->pref_.load(&this->voc_baselines_storage_)) { - ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, - this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); - } - - // Initialize storage timestamp - this->seconds_since_last_store_ = 0; - - if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) { - ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, - this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); - uint16_t states[4]; - - states[0] = this->voc_baselines_storage_.state0 >> 16; - states[1] = this->voc_baselines_storage_.state0 & 0xFFFF; - states[2] = this->voc_baselines_storage_.state1 >> 16; - states[3] = this->voc_baselines_storage_.state1 & 0xFFFF; - - if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) { - ESP_LOGE(TAG, "Failed to set VOC baseline from saved state"); + this->pref_ = global_preferences->make_preference(hash, true); + this->voc_baseline_time_ = App.get_loop_component_start_time(); + if (this->pref_.load(&this->voc_baseline_state_)) { + if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, this->voc_baseline_state_, 4)) { + ESP_LOGE(TAG, "VOC Baseline State write to sensor failed"); + } else { + ESP_LOGV(TAG, "VOC Baseline State loaded"); + delay(20); } } } @@ -288,6 +274,14 @@ void SEN5XComponent::dump_config() { ESP_LOGCONFIG(TAG, " RH/T acceleration mode: %s", LOG_STR_ARG(rht_accel_mode_to_string(this->acceleration_mode_.value()))); } + if (this->voc_sensor_) { + char hex_buf[5 * 4]; + format_hex_pretty_to(hex_buf, this->voc_baseline_state_, 4, 0); + ESP_LOGCONFIG(TAG, + " Store Baseline: %s\n" + " State: %s\n", + TRUEFALSE(this->store_baseline_), hex_buf); + } LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_); LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_); @@ -304,36 +298,6 @@ void SEN5XComponent::update() { return; } - // Store baselines after defined interval or if the difference between current and stored baseline becomes too - // much - if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) { - if (this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) { - // run it a bit later to avoid adding a delay here - this->set_timeout(550, [this]() { - uint16_t states[4]; - if (this->read_data(states, 4)) { - uint32_t state0 = states[0] << 16 | states[1]; - uint32_t state1 = states[2] << 16 | states[3]; - if ((uint32_t) std::abs(static_cast(this->voc_baselines_storage_.state0 - state0)) > - MAXIMUM_STORAGE_DIFF || - (uint32_t) std::abs(static_cast(this->voc_baselines_storage_.state1 - state1)) > - MAXIMUM_STORAGE_DIFF) { - this->seconds_since_last_store_ = 0; - this->voc_baselines_storage_.state0 = state0; - this->voc_baselines_storage_.state1 = state1; - - if (this->pref_.save(&this->voc_baselines_storage_)) { - ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, - this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); - } else { - ESP_LOGW(TAG, "Could not store VOC baselines"); - } - } - } - }); - } - } - if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) { this->status_set_warning(); ESP_LOGD(TAG, "Write error: read measurement (%d)", this->last_error_); @@ -402,7 +366,29 @@ void SEN5XComponent::update() { if (this->nox_sensor_ != nullptr) { this->nox_sensor_->publish_state(nox); } - this->status_clear_warning(); + + if (!this->voc_sensor_ || !this->store_baseline_ || + (App.get_loop_component_start_time() - this->voc_baseline_time_) < SHORTEST_BASELINE_STORE_INTERVAL) { + this->status_clear_warning(); + } else { + this->voc_baseline_time_ = App.get_loop_component_start_time(); + if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) { + this->status_set_warning(); + ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL); + } else { + this->set_timeout(20, [this]() { + if (!this->read_data(this->voc_baseline_state_, 4)) { + this->status_set_warning(); + ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL); + } else { + if (this->pref_.save(&this->voc_baseline_state_)) { + ESP_LOGD(TAG, "VOC Baseline State saved"); + } + this->status_clear_warning(); + } + }); + } + } }); } diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index 9e5b6bf231..aaa672dbc4 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -24,11 +24,6 @@ enum RhtAccelerationMode : uint16_t { HIGH_ACCELERATION = 2, }; -struct Sen5xBaselines { - int32_t state0; - int32_t state1; -} PACKED; // NOLINT - struct GasTuning { uint16_t index_offset; uint16_t learning_time_offset_hours; @@ -44,11 +39,9 @@ struct TemperatureCompensation { uint16_t time_constant; }; -// Shortest time interval of 3H for storing baseline values. +// Shortest time interval of 2H (in milliseconds) for storing baseline values. // Prevents wear of the flash because of too many write operations -static const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800; -// Store anyway if the baseline difference exceeds the max storage diff value -static const uint32_t MAXIMUM_STORAGE_DIFF = 50; +static const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 2 * 60 * 60 * 1000; class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: @@ -107,7 +100,8 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning); bool write_temperature_compensation_(const TemperatureCompensation &compensation); - uint32_t seconds_since_last_store_; + uint16_t voc_baseline_state_[4]{0}; + uint32_t voc_baseline_time_; uint16_t firmware_version_; ERRORCODE error_code_; uint8_t serial_number_[4]; @@ -132,7 +126,6 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri optional temperature_compensation_; ESPPreferenceObject pref_; std::string product_name_; - Sen5xBaselines voc_baselines_storage_; }; } // namespace sen5x diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py index 9c3114b9e2..538a2f5239 100644 --- a/esphome/components/sen5x/sensor.py +++ b/esphome/components/sen5x/sensor.py @@ -210,6 +210,7 @@ SENSOR_MAP = { SETTING_MAP = { CONF_AUTO_CLEANING_INTERVAL: "set_auto_cleaning_interval", CONF_ACCELERATION_MODE: "set_acceleration_mode", + CONF_STORE_BASELINE: "set_store_baseline", }