ESPHome 2026.5.0b1
Loading...
Searching...
No Matches
sgp4x.cpp
Go to the documentation of this file.
1#include "sgp4x.h"
3#include "esphome/core/log.h"
4#include "esphome/core/hal.h"
5#include <cinttypes>
6
7namespace esphome::sgp4x {
8
9static const char *const TAG = "sgp4x";
10
12 // Serial Number identification
13 uint16_t raw_serial_number[3];
14 if (!this->get_register(SGP4X_CMD_GET_SERIAL_ID, raw_serial_number, 3, 1)) {
15 ESP_LOGE(TAG, "Get serial number failed");
16 this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED;
17 this->mark_failed();
18 return;
19 }
20 this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) |
21 (uint64_t(raw_serial_number[2]));
22 ESP_LOGD(TAG, "Serial number: %" PRIu64, this->serial_number_);
23
24 // Featureset identification for future use
25 uint16_t featureset;
26 if (!this->get_register(SGP4X_CMD_GET_FEATURESET, featureset, 1)) {
27 ESP_LOGD(TAG, "Get feature set failed");
28 this->mark_failed();
29 return;
30 }
31 featureset &= 0x1FF;
32 if (featureset == SGP40_FEATURESET) {
33 this->sgp_type_ = SGP40;
34 this->self_test_time_ = SPG40_SELFTEST_TIME;
35 this->measure_time_ = SGP40_MEASURE_TIME;
36 if (this->nox_sensor_) {
37 ESP_LOGE(TAG, "SGP41 required for NOx, disabling NOx sensor");
38 // Drop the pointer so update() never publishes to it.
39 // The entity remains registered but will never receive state updates.
40 this->nox_sensor_ = nullptr;
41 }
42 } else if (featureset == SGP41_FEATURESET) {
43 this->sgp_type_ = SGP41;
44 this->self_test_time_ = SPG41_SELFTEST_TIME;
45 this->measure_time_ = SGP41_MEASURE_TIME;
46 } else {
47 ESP_LOGD(TAG, "Unknown feature set 0x%0X", featureset);
48 this->mark_failed();
49 return;
50 }
51
52 ESP_LOGD(TAG, "Version 0x%0X", featureset);
53
54 if (this->store_baseline_) {
55 // Hash with config hash, version, and serial number
56 // This ensures the baseline storage is cleared after OTA
57 // Serial numbers are unique to each sensor, so multiple sensors can be used without conflict
58 uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), this->serial_number_);
60
61 if (this->pref_.load(&this->voc_baselines_storage_)) {
64 ESP_LOGV(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
66 }
67
68 // Initialize storage timestamp
70
71 if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) {
72 ESP_LOGV(TAG, "Setting VOC baseline from save state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
74 voc_algorithm_.set_states(this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
75 }
76 }
77 if (this->voc_sensor_ && this->voc_tuning_params_.has_value()) {
78 voc_algorithm_.set_tuning_parameters(
79 voc_tuning_params_.value().index_offset, voc_tuning_params_.value().learning_time_offset_hours,
80 voc_tuning_params_.value().learning_time_gain_hours, voc_tuning_params_.value().gating_max_duration_minutes,
81 voc_tuning_params_.value().std_initial, voc_tuning_params_.value().gain_factor);
82 }
83
84 if (this->nox_sensor_ && this->nox_tuning_params_.has_value()) {
85 nox_algorithm_.set_tuning_parameters(
86 nox_tuning_params_.value().index_offset, nox_tuning_params_.value().learning_time_offset_hours,
87 nox_tuning_params_.value().learning_time_gain_hours, nox_tuning_params_.value().gating_max_duration_minutes,
88 nox_tuning_params_.value().std_initial, nox_tuning_params_.value().gain_factor);
89 }
90
91 this->self_test_();
92
93 /* The official spec for this sensor at
94 https://sensirion.com/media/documents/296373BB/6203C5DF/Sensirion_Gas_Sensors_Datasheet_SGP40.pdf indicates this
95 sensor should be driven at 1Hz. Comments from the developers at:
96 https://github.com/Sensirion/embedded-sgp/issues/136 indicate the algorithm should be a bit resilient to slight
97 timing variations so the software timer should be accurate enough for this.
98
99 This block starts sampling from the sensor at 1Hz, and is done separately from the call
100 to the update method. This separation is to support getting accurate measurements but
101 limit the amount of communication done over wifi for power consumption or to keep the
102 number of records reported from being overwhelming.
103 */
104 ESP_LOGV(TAG, "Component requires sampling of 1Hz, setting up background sampler");
105 this->set_interval(1000, [this]() { this->take_sample(); });
106}
107
109 ESP_LOGD(TAG, "Starting self-test");
110 if (!this->write_command(SGP4X_CMD_SELF_TEST)) {
111 this->error_code_ = COMMUNICATION_FAILED;
112 ESP_LOGD(TAG, ESP_LOG_MSG_COMM_FAIL);
113 this->mark_failed();
114 }
115
116 this->set_timeout(this->self_test_time_, [this]() {
117 uint16_t reply = 0;
118 if (!this->read_data(reply) || (reply != 0xD400)) {
119 this->error_code_ = SELF_TEST_FAILED;
120 ESP_LOGW(TAG, "Self-test failed (0x%X)", reply);
121 this->mark_failed();
122 return;
123 }
124
125 this->self_test_complete_ = true;
127 ESP_LOGD(TAG, "Self-test complete");
128 });
129}
130
132 this->voc_index_ = this->voc_algorithm_.process(this->voc_sraw_);
133 if (this->nox_sensor_ != nullptr)
134 this->nox_index_ = this->nox_algorithm_.process(this->nox_sraw_);
135 ESP_LOGV(TAG, "VOC: %" PRId32 ", NOx: %" PRId32, this->voc_index_, this->nox_index_);
136 // Store baselines after defined interval or if the difference between current and stored baseline becomes too
137 // much
139 this->voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_);
140 if (std::abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF ||
141 std::abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF) {
145
146 if (this->pref_.save(&this->voc_baselines_storage_)) {
147 ESP_LOGV(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
148 this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
149 } else {
150 ESP_LOGW(TAG, "Storing VOC baselines failed");
151 }
152 }
153 }
154
156 this->samples_read_++;
157 ESP_LOGD(TAG, "Stabilizing (%d/%d); VOC index: %" PRIu32, this->samples_read_, this->samples_to_stabilize_,
158 this->voc_index_);
159 }
160}
161
163 float humidity = NAN;
164
165 if (!this->self_test_complete_) {
166 ESP_LOGW(TAG, "Self-test incomplete");
167 return;
168 }
169 if (this->humidity_sensor_ != nullptr) {
170 humidity = this->humidity_sensor_->state;
171 }
172 if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
173 humidity = 50;
174 }
175
176 float temperature = NAN;
177 if (this->temperature_sensor_ != nullptr) {
178 temperature = float(this->temperature_sensor_->state);
179 }
180 if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
181 temperature = 25;
182 }
183
184 uint16_t command;
185 uint16_t data[2];
186 size_t response_words;
187 // Use SGP40 measure command if we don't care about NOx
188 if (nox_sensor_ == nullptr) {
189 command = SGP40_CMD_MEASURE_RAW;
190 response_words = 1;
191 } else {
192 // SGP41 sensor must use NOx conditioning command for the first 10 seconds
193 if (this->nox_conditioning_start_.has_value() && millis() - *this->nox_conditioning_start_ < 10000) {
194 command = SGP41_CMD_NOX_CONDITIONING;
195 response_words = 1;
196 } else {
197 this->nox_conditioning_start_.reset();
198 command = SGP41_CMD_MEASURE_RAW;
199 response_words = 2;
200 }
201 }
202 uint16_t rhticks = (uint16_t) llround((humidity * 65535) / 100);
203 uint16_t tempticks = (uint16_t) (((temperature + 45) * 65535) / 175);
204 // first parameter are the relative humidity ticks
205 data[0] = rhticks;
206 // secomd parameter are the temperature ticks
207 data[1] = tempticks;
208
209 if (!this->write_command(command, data, 2)) {
210 ESP_LOGD(TAG, "write error (%d)", this->last_error_);
211 this->status_set_warning(LOG_STR("measurement request failed"));
212 return;
213 }
214
215 this->set_timeout(this->measure_time_, [this, response_words]() {
216 uint16_t raw_data[2];
217 raw_data[1] = 0;
218 if (!this->read_data(raw_data, response_words)) {
219 ESP_LOGD(TAG, "read error (%d)", this->last_error_);
220 this->status_set_warning(LOG_STR("measurement read failed"));
221 this->voc_index_ = this->nox_index_ = UINT16_MAX;
222 return;
223 }
224 this->voc_sraw_ = raw_data[0];
225 this->nox_sraw_ = raw_data[1]; // either 0 or the measured NOx ticks
226 this->status_clear_warning();
227 this->update_gas_indices_();
228 });
229}
230
232 if (!this->self_test_complete_)
233 return;
234 this->seconds_since_last_store_ += 1;
235 this->measure_raw_();
236}
237
240 return;
241 }
242 if (this->voc_sensor_ != nullptr) {
243 if (this->voc_index_ != UINT16_MAX)
245 }
246 if (this->nox_sensor_ != nullptr) {
247 if (this->nox_index_ != UINT16_MAX)
249 }
250}
251
253 ESP_LOGCONFIG(TAG, "SGP4x:");
254 LOG_I2C_DEVICE(this);
255 ESP_LOGCONFIG(TAG, " Store baseline: %s", YESNO(this->store_baseline_));
256
257 if (this->is_failed()) {
258 switch (this->error_code_) {
259 case COMMUNICATION_FAILED:
260 ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
261 break;
262 case SERIAL_NUMBER_IDENTIFICATION_FAILED:
263 ESP_LOGW(TAG, "Get serial number failed");
264 break;
265 case SELF_TEST_FAILED:
266 ESP_LOGW(TAG, "Self-test failed");
267 break;
268 default:
269 ESP_LOGW(TAG, "Unknown error");
270 break;
271 }
272 } else {
273 ESP_LOGCONFIG(TAG,
274 " Type: %s\n"
275 " Serial number: %" PRIu64 "\n"
276 " Minimum Samples: %f",
277 sgp_type_ == SGP41 ? "SGP41" : "SPG40", this->serial_number_, GasIndexAlgorithm_INITIAL_BLACKOUT);
278 }
279 LOG_UPDATE_INTERVAL(this);
280
281 ESP_LOGCONFIG(TAG, " Compensation:");
282 if (this->humidity_sensor_ != nullptr || this->temperature_sensor_ != nullptr) {
283 LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_);
284 LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_);
285 } else {
286 ESP_LOGCONFIG(TAG, " No source configured");
287 }
288 LOG_SENSOR(" ", "VOC", this->voc_sensor_);
289 LOG_SENSOR(" ", "NOx", this->nox_sensor_);
290}
291
292} // namespace esphome::sgp4x
uint32_t get_config_version_hash()
Get the config hash extended with ESPHome version.
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:284
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_timeout(const std voi set_timeout)(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a unique name.
Definition component.h:510
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_interval(const std voi set_interval)(const char *name, uint32_t interval, std::function< void()> &&f)
Set an interval function with a unique name.
Definition component.h:417
void status_clear_warning()
Definition component.h:306
i2c::ErrorCode last_error_
last error code from I2C operation
bool get_register(uint16_t command, uint16_t *data, uint8_t len, uint8_t delay=0)
get data words from I2C register.
bool write_command(T i2c_register)
Write a command to the I2C device.
bool read_data(uint16_t *data, uint8_t len)
Read data words from I2C device.
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:68
float state
This member variable stores the last state that has passed through all filters.
Definition sensor.h:138
SGP4xBaselines voc_baselines_storage_
Definition sgp4x.h:134
optional< uint32_t > nox_conditioning_start_
Definition sgp4x.h:131
ESPPreferenceObject pref_
Definition sgp4x.h:132
void dump_config() override
Definition sgp4x.cpp:252
sensor::Sensor * humidity_sensor_
Input sensor for humidity and temperature compensation.
Definition sgp4x.h:99
sensor::Sensor * voc_sensor_
Definition sgp4x.h:114
VOCGasIndexAlgorithm voc_algorithm_
Definition sgp4x.h:115
sensor::Sensor * temperature_sensor_
Definition sgp4x.h:100
optional< GasTuning > voc_tuning_params_
Definition sgp4x.h:116
NOxGasIndexAlgorithm nox_algorithm_
Definition sgp4x.h:123
optional< GasTuning > nox_tuning_params_
Definition sgp4x.h:124
sensor::Sensor * nox_sensor_
Definition sgp4x.h:121
const uint32_t SHORTEST_BASELINE_STORE_INTERVAL
Definition sgp4x.h:46
const float MAXIMUM_STORAGE_DIFF
Definition sgp4x.h:52
constexpr uint32_t fnv1a_hash_extend(uint32_t hash, const char *str)
Extend a FNV-1a hash with additional string data.
Definition helpers.h:811
ESPPreferences * global_preferences
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
ESPPreferenceObject make_preference(size_t, uint32_t, bool)
Definition preferences.h:24
uint16_t temperature
Definition sun_gtil2.cpp:12