ESPHome 2026.11.0-dev
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#include <cmath>
7
8namespace esphome::sgp4x {
9
10ESPHOME_LOG_TAG(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 this->mark_failed(LOG_STR("Get serial number failed"));
17 return;
18 }
19 this->serial_number_ = (uint64_t(raw_serial_number[0]) << 32) | (uint64_t(raw_serial_number[1]) << 16) |
20 (uint64_t(raw_serial_number[2]));
21 ESP_LOGD(TAG, "Serial number: %" PRIu64, this->serial_number_);
22
23 // Featureset identification for future use
24 uint16_t featureset;
25 if (!this->get_register(SGP4X_CMD_GET_FEATURESET, featureset, 1)) {
26 ESP_LOGD(TAG, "Get feature set failed");
27 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
28 return;
29 }
30 featureset &= 0x1FF;
31 if (featureset == SGP40_FEATURESET) {
32 this->sgp_type_ = SGP40;
33 this->measure_time_ = SGP40_MEASURE_TIME;
34 if (this->nox_sensor_) {
35 ESP_LOGE(TAG, "SGP41 required for NOx, disabling NOx sensor");
36 // Drop the pointer so update() never publishes to it.
37 // The entity remains registered but will never receive state updates.
38 this->nox_sensor_ = nullptr;
39 }
40 } else if (featureset == SGP41_FEATURESET) {
41 this->sgp_type_ = SGP41;
42 this->measure_time_ = SGP41_MEASURE_TIME;
43 } else {
44 ESP_LOGD(TAG, "Unknown feature set 0x%0X", featureset);
45 this->mark_failed(LOG_STR("Unknown feature set"));
46 return;
47 }
48
49 ESP_LOGD(TAG, "Version 0x%0X", featureset);
50
51 if (this->voc_sensor_ && this->voc_tuning_params_.has_value()) {
52 voc_algorithm_.set_tuning_parameters(
53 voc_tuning_params_.value().index_offset, voc_tuning_params_.value().learning_time_offset_hours,
54 voc_tuning_params_.value().learning_time_gain_hours, voc_tuning_params_.value().gating_max_duration_minutes,
55 voc_tuning_params_.value().std_initial, voc_tuning_params_.value().gain_factor);
56 }
57
58 if (this->nox_sensor_ && this->nox_tuning_params_.has_value()) {
59 nox_algorithm_.set_tuning_parameters(
60 nox_tuning_params_.value().index_offset, nox_tuning_params_.value().learning_time_offset_hours,
61 nox_tuning_params_.value().learning_time_gain_hours, nox_tuning_params_.value().gating_max_duration_minutes,
62 nox_tuning_params_.value().std_initial, nox_tuning_params_.value().gain_factor);
63 }
64
65 if (this->store_baseline_) {
66 // Initialize storage timestamp
68
69 // Hash with config hash, version, and serial number
70 // This ensures the baseline storage is cleared after OTA
71 // Serial numbers are unique to each sensor, so multiple sensors can be used without conflict
72 uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), this->serial_number_);
74
75 if (this->pref_.load(&this->voc_baselines_storage_)) {
78
79 ESP_LOGV(TAG, "Loaded VOC baseline state0: %f, state1: %f", this->voc_baselines_storage_.state0,
80 this->voc_baselines_storage_.state1);
81
82 if (std::isnormal(this->voc_baselines_storage_.state0) && std::isnormal(this->voc_baselines_storage_.state1)) {
83 ESP_LOGV(TAG, "Setting VOC baseline from save state0: %f, state1: %f", this->voc_baselines_storage_.state0,
84 this->voc_baselines_storage_.state1);
85 // Sensirion advises restoring states only after interruptions shorter than 10 minutes; with no way to know
86 // how long the device was off, restoring a stale state still beats a fresh 12-hour learning phase
87 voc_algorithm_.set_states(this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
88 }
89 }
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->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
113 return;
114 }
115
116 this->set_timeout(SGP4X_SELF_TEST_TIME, [this]() {
117 uint16_t reply = 0;
118 // SGP40: MSB is 0xD4 on success, LSB is undefined; SGP41: MSB is undefined, LSB bits 0/1 flag VOC/NOx pixel
119 // failures
120 bool passed = this->read_data(reply) && (this->sgp_type_ == SGP41 ? (reply & 0x0003) == 0 : (reply >> 8) == 0xD4);
121 if (!passed) {
122 ESP_LOGW(TAG, "Self-test failed (0x%X)", reply);
123 this->mark_failed(LOG_STR("Self-test failed"));
124 return;
125 }
126
127 this->self_test_complete_ = true;
129 ESP_LOGD(TAG, "Self-test complete");
130 });
131}
132
134 this->voc_index_ = this->voc_algorithm_.process(this->voc_sraw_);
135 if (this->nox_sensor_ != nullptr)
136 this->nox_index_ = this->nox_algorithm_.process(this->nox_sraw_);
137 ESP_LOGV(TAG, "VOC: %" PRId32 ", NOx: %" PRId32, this->voc_index_, this->nox_index_);
138 // Store baselines once the minimum interval has passed and the state has drifted from the stored copy;
139 // both conditions limit flash wear
141 this->voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_);
142 if (std::abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF_STATE0 ||
143 std::abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF_STATE1) {
147
148 if (this->pref_.save(&this->voc_baselines_storage_)) {
149 ESP_LOGV(TAG, "Stored VOC baseline state0: %f, state1: %f", this->voc_baselines_storage_.state0,
150 this->voc_baselines_storage_.state1);
151 } else {
152 ESP_LOGW(TAG, "Storing VOC baselines failed");
153 }
154 }
155 }
156
158 this->samples_read_++;
159 ESP_LOGD(TAG, "Stabilizing (%d/%d); VOC index: %" PRIu32, this->samples_read_, this->samples_to_stabilize_,
160 this->voc_index_);
161 }
162}
163
165 float humidity = NAN;
166
167 if (!this->self_test_complete_) {
168 ESP_LOGW(TAG, "Self-test incomplete");
169 return;
170 }
171 if (this->humidity_sensor_ != nullptr) {
172 humidity = this->humidity_sensor_->state;
173 }
174 if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
175 humidity = 50;
176 }
177
178 float temperature = NAN;
179 if (this->temperature_sensor_ != nullptr) {
180 temperature = float(this->temperature_sensor_->state);
181 }
182 if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
183 temperature = 25;
184 }
185
186 uint16_t command;
187 uint16_t data[2];
188 size_t response_words;
189 if (this->sgp_type_ == SGP40) {
190 command = SGP40_CMD_MEASURE_RAW;
191 response_words = 1;
192 } else if (this->nox_conditioning_start_.has_value() && millis() - *this->nox_conditioning_start_ < 10000) {
193 // SGP41 must run the NOx conditioning command for the first 10 seconds
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 if (command == SGP41_CMD_NOX_CONDITIONING) {
202 // Conditioning requires the default parameters (compensation disabled)
203 data[0] = 0x8000;
204 data[1] = 0x6666;
205 } else {
206 // first parameter are the relative humidity ticks
207 data[0] = (uint16_t) std::llround((humidity * 65535) / 100);
208 // second parameter are the temperature ticks
209 data[1] = (uint16_t) (((temperature + 45) * 65535) / 175);
210 }
211
212 if (!this->write_command(command, data, 2)) {
213 ESP_LOGD(TAG, "write error (%d)", this->last_error_);
214 this->status_set_warning(LOG_STR("measurement request failed"));
215 return;
216 }
217
218 this->set_timeout(this->measure_time_, [this, response_words]() {
219 uint16_t raw_data[2];
220 raw_data[1] = 0;
221 if (!this->read_data(raw_data, response_words)) {
222 ESP_LOGD(TAG, "read error (%d)", this->last_error_);
223 this->status_set_warning(LOG_STR("measurement read failed"));
224 this->voc_index_ = this->nox_index_ = UINT16_MAX;
225 return;
226 }
227 this->voc_sraw_ = raw_data[0];
228 this->nox_sraw_ = raw_data[1]; // either 0 or the measured NOx ticks
229 this->status_clear_warning();
230 this->update_gas_indices_();
231 });
232}
233
235 if (!this->self_test_complete_)
236 return;
237 if (this->store_baseline_) {
238 this->seconds_since_last_store_ += 1;
239 }
240 this->measure_raw_();
241}
242
245 return;
246 }
247 if (this->voc_sensor_ != nullptr) {
248 if (this->voc_index_ != UINT16_MAX)
250 }
251 if (this->nox_sensor_ != nullptr) {
252 if (this->nox_index_ != UINT16_MAX)
254 }
255}
256
258 ESP_LOGCONFIG(TAG, "SGP4x:");
259 LOG_I2C_DEVICE(this);
260 ESP_LOGCONFIG(TAG, " Store baseline: %s", YESNO(this->store_baseline_));
261
262 if (!this->is_failed()) {
263 ESP_LOGCONFIG(TAG,
264 " Type: %s\n"
265 " Serial number: %" PRIu64 "\n"
266 " Minimum Samples: %f",
267 this->sgp_type_ == SGP41 ? LOG_STR_LITERAL("SGP41") : LOG_STR_LITERAL("SGP40"), this->serial_number_,
268 GasIndexAlgorithm_INITIAL_BLACKOUT);
269 }
270 LOG_UPDATE_INTERVAL(this);
271
272 ESP_LOGCONFIG(TAG, " Compensation:");
273 if (this->humidity_sensor_ != nullptr || this->temperature_sensor_ != nullptr) {
274 LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_);
275 LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_);
276 } else {
277 ESP_LOGCONFIG(TAG, " No source configured");
278 }
279 LOG_SENSOR(" ", "VOC", this->voc_sensor_);
280 LOG_SENSOR(" ", "NOx", this->nox_sensor_);
281}
282
283} // 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:274
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
Definition component.cpp:96
void set_interval(const char *name, uint32_t interval, std::function< void()> &&f)
Set an interval function with a const char* name.
Definition component.cpp:88
void status_clear_warning()
Definition component.h:296
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:140
SGP4xBaselines voc_baselines_storage_
Definition sgp4x.h:127
optional< uint32_t > nox_conditioning_start_
Definition sgp4x.h:124
ESPPreferenceObject pref_
Definition sgp4x.h:125
void dump_config() override
Definition sgp4x.cpp:257
sensor::Sensor * humidity_sensor_
Input sensor for humidity and temperature compensation.
Definition sgp4x.h:93
sensor::Sensor * voc_sensor_
Definition sgp4x.h:107
VOCGasIndexAlgorithm voc_algorithm_
Definition sgp4x.h:108
sensor::Sensor * temperature_sensor_
Definition sgp4x.h:94
optional< GasTuning > voc_tuning_params_
Definition sgp4x.h:109
NOxGasIndexAlgorithm nox_algorithm_
Definition sgp4x.h:116
optional< GasTuning > nox_tuning_params_
Definition sgp4x.h:117
sensor::Sensor * nox_sensor_
Definition sgp4x.h:114
ESPHOME_LOG_TAG(TAG, "sgp4x")
const float MAXIMUM_STORAGE_DIFF_STATE0
Definition sgp4x.h:51
const float MAXIMUM_STORAGE_DIFF_STATE1
Definition sgp4x.h:54
const uint32_t SHORTEST_BASELINE_STORE_INTERVAL
Definition sgp4x.h:45
constexpr uint32_t fnv1a_hash_extend(uint32_t hash, const char *str)
Extend a FNV-1a hash with additional string data.
Definition helpers.h:950
ESPPreferences * global_preferences
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
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