ESPHome 2026.8.0b4
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
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]) << 32) | (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->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->measure_time_ = SGP41_MEASURE_TIME;
45 } else {
46 ESP_LOGD(TAG, "Unknown feature set 0x%0X", featureset);
47 this->mark_failed();
48 return;
49 }
50
51 ESP_LOGD(TAG, "Version 0x%0X", featureset);
52
53 if (this->voc_sensor_ && this->voc_tuning_params_.has_value()) {
54 voc_algorithm_.set_tuning_parameters(
55 voc_tuning_params_.value().index_offset, voc_tuning_params_.value().learning_time_offset_hours,
56 voc_tuning_params_.value().learning_time_gain_hours, voc_tuning_params_.value().gating_max_duration_minutes,
57 voc_tuning_params_.value().std_initial, voc_tuning_params_.value().gain_factor);
58 }
59
60 if (this->nox_sensor_ && this->nox_tuning_params_.has_value()) {
61 nox_algorithm_.set_tuning_parameters(
62 nox_tuning_params_.value().index_offset, nox_tuning_params_.value().learning_time_offset_hours,
63 nox_tuning_params_.value().learning_time_gain_hours, nox_tuning_params_.value().gating_max_duration_minutes,
64 nox_tuning_params_.value().std_initial, nox_tuning_params_.value().gain_factor);
65 }
66
67 if (this->store_baseline_) {
68 // Initialize storage timestamp
70
71 // Hash with config hash, version, and serial number
72 // This ensures the baseline storage is cleared after OTA
73 // Serial numbers are unique to each sensor, so multiple sensors can be used without conflict
74 uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), this->serial_number_);
76
77 if (this->pref_.load(&this->voc_baselines_storage_)) {
80
81 ESP_LOGV(TAG, "Loaded VOC baseline state0: %f, state1: %f", this->voc_baselines_storage_.state0,
82 this->voc_baselines_storage_.state1);
83
84 if (std::isnormal(this->voc_baselines_storage_.state0) && std::isnormal(this->voc_baselines_storage_.state1)) {
85 ESP_LOGV(TAG, "Setting VOC baseline from save state0: %f, state1: %f", this->voc_baselines_storage_.state0,
86 this->voc_baselines_storage_.state1);
87 // Sensirion advises restoring states only after interruptions shorter than 10 minutes; with no way to know
88 // how long the device was off, restoring a stale state still beats a fresh 12-hour learning phase
89 voc_algorithm_.set_states(this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
90 }
91 }
92 }
93
94 this->self_test_();
95
96 /* The official spec for this sensor at
97 https://sensirion.com/media/documents/296373BB/6203C5DF/Sensirion_Gas_Sensors_Datasheet_SGP40.pdf indicates this
98 sensor should be driven at 1Hz. Comments from the developers at:
99 https://github.com/Sensirion/embedded-sgp/issues/136 indicate the algorithm should be a bit resilient to slight
100 timing variations so the software timer should be accurate enough for this.
101
102 This block starts sampling from the sensor at 1Hz, and is done separately from the call
103 to the update method. This separation is to support getting accurate measurements but
104 limit the amount of communication done over wifi for power consumption or to keep the
105 number of records reported from being overwhelming.
106 */
107 ESP_LOGV(TAG, "Component requires sampling of 1Hz, setting up background sampler");
108 this->set_interval(1000, [this]() { this->take_sample(); });
109}
110
112 ESP_LOGD(TAG, "Starting self-test");
113 if (!this->write_command(SGP4X_CMD_SELF_TEST)) {
114 this->error_code_ = COMMUNICATION_FAILED;
115 ESP_LOGD(TAG, ESP_LOG_MSG_COMM_FAIL);
116 this->mark_failed();
117 return;
118 }
119
120 this->set_timeout(SGP4X_SELF_TEST_TIME, [this]() {
121 uint16_t reply = 0;
122 // SGP40: MSB is 0xD4 on success, LSB is undefined; SGP41: MSB is undefined, LSB bits 0/1 flag VOC/NOx pixel
123 // failures
124 bool passed = this->read_data(reply) && (this->sgp_type_ == SGP41 ? (reply & 0x0003) == 0 : (reply >> 8) == 0xD4);
125 if (!passed) {
126 this->error_code_ = SELF_TEST_FAILED;
127 ESP_LOGW(TAG, "Self-test failed (0x%X)", reply);
128 this->mark_failed();
129 return;
130 }
131
132 this->self_test_complete_ = true;
134 ESP_LOGD(TAG, "Self-test complete");
135 });
136}
137
139 this->voc_index_ = this->voc_algorithm_.process(this->voc_sraw_);
140 if (this->nox_sensor_ != nullptr)
141 this->nox_index_ = this->nox_algorithm_.process(this->nox_sraw_);
142 ESP_LOGV(TAG, "VOC: %" PRId32 ", NOx: %" PRId32, this->voc_index_, this->nox_index_);
143 // Store baselines once the minimum interval has passed and the state has drifted from the stored copy;
144 // both conditions limit flash wear
146 this->voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_);
147 if (std::abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF_STATE0 ||
148 std::abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF_STATE1) {
152
153 if (this->pref_.save(&this->voc_baselines_storage_)) {
154 ESP_LOGV(TAG, "Stored VOC baseline state0: %f, state1: %f", this->voc_baselines_storage_.state0,
155 this->voc_baselines_storage_.state1);
156 } else {
157 ESP_LOGW(TAG, "Storing VOC baselines failed");
158 }
159 }
160 }
161
163 this->samples_read_++;
164 ESP_LOGD(TAG, "Stabilizing (%d/%d); VOC index: %" PRIu32, this->samples_read_, this->samples_to_stabilize_,
165 this->voc_index_);
166 }
167}
168
170 float humidity = NAN;
171
172 if (!this->self_test_complete_) {
173 ESP_LOGW(TAG, "Self-test incomplete");
174 return;
175 }
176 if (this->humidity_sensor_ != nullptr) {
177 humidity = this->humidity_sensor_->state;
178 }
179 if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
180 humidity = 50;
181 }
182
183 float temperature = NAN;
184 if (this->temperature_sensor_ != nullptr) {
185 temperature = float(this->temperature_sensor_->state);
186 }
187 if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
188 temperature = 25;
189 }
190
191 uint16_t command;
192 uint16_t data[2];
193 size_t response_words;
194 if (this->sgp_type_ == SGP40) {
195 command = SGP40_CMD_MEASURE_RAW;
196 response_words = 1;
197 } else if (this->nox_conditioning_start_.has_value() && millis() - *this->nox_conditioning_start_ < 10000) {
198 // SGP41 must run the NOx conditioning command for the first 10 seconds
199 command = SGP41_CMD_NOX_CONDITIONING;
200 response_words = 1;
201 } else {
202 this->nox_conditioning_start_.reset();
203 command = SGP41_CMD_MEASURE_RAW;
204 response_words = 2;
205 }
206 if (command == SGP41_CMD_NOX_CONDITIONING) {
207 // Conditioning requires the default parameters (compensation disabled)
208 data[0] = 0x8000;
209 data[1] = 0x6666;
210 } else {
211 // first parameter are the relative humidity ticks
212 data[0] = (uint16_t) std::llround((humidity * 65535) / 100);
213 // second parameter are the temperature ticks
214 data[1] = (uint16_t) (((temperature + 45) * 65535) / 175);
215 }
216
217 if (!this->write_command(command, data, 2)) {
218 ESP_LOGD(TAG, "write error (%d)", this->last_error_);
219 this->status_set_warning(LOG_STR("measurement request failed"));
220 return;
221 }
222
223 this->set_timeout(this->measure_time_, [this, response_words]() {
224 uint16_t raw_data[2];
225 raw_data[1] = 0;
226 if (!this->read_data(raw_data, response_words)) {
227 ESP_LOGD(TAG, "read error (%d)", this->last_error_);
228 this->status_set_warning(LOG_STR("measurement read failed"));
229 this->voc_index_ = this->nox_index_ = UINT16_MAX;
230 return;
231 }
232 this->voc_sraw_ = raw_data[0];
233 this->nox_sraw_ = raw_data[1]; // either 0 or the measured NOx ticks
234 this->status_clear_warning();
235 this->update_gas_indices_();
236 });
237}
238
240 if (!this->self_test_complete_)
241 return;
242 if (this->store_baseline_) {
243 this->seconds_since_last_store_ += 1;
244 }
245 this->measure_raw_();
246}
247
250 return;
251 }
252 if (this->voc_sensor_ != nullptr) {
253 if (this->voc_index_ != UINT16_MAX)
255 }
256 if (this->nox_sensor_ != nullptr) {
257 if (this->nox_index_ != UINT16_MAX)
259 }
260}
261
263 ESP_LOGCONFIG(TAG, "SGP4x:");
264 LOG_I2C_DEVICE(this);
265 ESP_LOGCONFIG(TAG, " Store baseline: %s", YESNO(this->store_baseline_));
266
267 if (this->is_failed()) {
268 switch (this->error_code_) {
269 case COMMUNICATION_FAILED:
270 ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
271 break;
272 case SERIAL_NUMBER_IDENTIFICATION_FAILED:
273 ESP_LOGW(TAG, "Get serial number failed");
274 break;
275 case SELF_TEST_FAILED:
276 ESP_LOGW(TAG, "Self-test failed");
277 break;
278 default:
279 ESP_LOGW(TAG, "Unknown error");
280 break;
281 }
282 } else {
283 ESP_LOGCONFIG(TAG,
284 " Type: %s\n"
285 " Serial number: %" PRIu64 "\n"
286 " Minimum Samples: %f",
287 this->sgp_type_ == SGP41 ? "SGP41" : "SGP40", this->serial_number_,
288 GasIndexAlgorithm_INITIAL_BLACKOUT);
289 }
290 LOG_UPDATE_INTERVAL(this);
291
292 ESP_LOGCONFIG(TAG, " Compensation:");
293 if (this->humidity_sensor_ != nullptr || this->temperature_sensor_ != nullptr) {
294 LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_);
295 LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_);
296 } else {
297 ESP_LOGCONFIG(TAG, " No source configured");
298 }
299 LOG_SENSOR(" ", "VOC", this->voc_sensor_);
300 LOG_SENSOR(" ", "NOx", this->nox_sensor_);
301}
302
303} // 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:272
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:289
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:137
optional< uint32_t > nox_conditioning_start_
Definition sgp4x.h:134
ESPPreferenceObject pref_
Definition sgp4x.h:135
void dump_config() override
Definition sgp4x.cpp:262
sensor::Sensor * humidity_sensor_
Input sensor for humidity and temperature compensation.
Definition sgp4x.h:103
sensor::Sensor * voc_sensor_
Definition sgp4x.h:117
VOCGasIndexAlgorithm voc_algorithm_
Definition sgp4x.h:118
sensor::Sensor * temperature_sensor_
Definition sgp4x.h:104
optional< GasTuning > voc_tuning_params_
Definition sgp4x.h:119
NOxGasIndexAlgorithm nox_algorithm_
Definition sgp4x.h:126
optional< GasTuning > nox_tuning_params_
Definition sgp4x.h:127
sensor::Sensor * nox_sensor_
Definition sgp4x.h:124
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:835
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