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