ESPHome 2026.8.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
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->voc_sensor_ && this->voc_tuning_params_.has_value()) {
56 voc_algorithm_.set_tuning_parameters(
57 voc_tuning_params_.value().index_offset, voc_tuning_params_.value().learning_time_offset_hours,
58 voc_tuning_params_.value().learning_time_gain_hours, voc_tuning_params_.value().gating_max_duration_minutes,
59 voc_tuning_params_.value().std_initial, voc_tuning_params_.value().gain_factor);
60 }
61
62 if (this->nox_sensor_ && this->nox_tuning_params_.has_value()) {
63 nox_algorithm_.set_tuning_parameters(
64 nox_tuning_params_.value().index_offset, nox_tuning_params_.value().learning_time_offset_hours,
65 nox_tuning_params_.value().learning_time_gain_hours, nox_tuning_params_.value().gating_max_duration_minutes,
66 nox_tuning_params_.value().std_initial, nox_tuning_params_.value().gain_factor);
67 }
68
69 if (this->store_baseline_) {
70 // Initialize storage timestamp
72
73 // Hash with config hash, version, and serial number
74 // This ensures the baseline storage is cleared after OTA
75 // Serial numbers are unique to each sensor, so multiple sensors can be used without conflict
76 uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), this->serial_number_);
78
79 if (this->pref_.load(&this->voc_baselines_storage_)) {
82
83 ESP_LOGV(TAG, "Loaded VOC baseline state0: %f, state1: %f", this->voc_baselines_storage_.state0,
84 this->voc_baselines_storage_.state1);
85
86 if (std::isnormal(this->voc_baselines_storage_.state0) && std::isnormal(this->voc_baselines_storage_.state1)) {
87 ESP_LOGV(TAG, "Setting VOC baseline from save state0: %f, state1: %f", this->voc_baselines_storage_.state0,
88 this->voc_baselines_storage_.state1);
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 }
118
119 this->set_timeout(this->self_test_time_, [this]() {
120 uint16_t reply = 0;
121 if (!this->read_data(reply) || (reply != 0xD400)) {
122 this->error_code_ = SELF_TEST_FAILED;
123 ESP_LOGW(TAG, "Self-test failed (0x%X)", reply);
124 this->mark_failed();
125 return;
126 }
127
128 this->self_test_complete_ = true;
130 ESP_LOGD(TAG, "Self-test complete");
131 });
132}
133
135 this->voc_index_ = this->voc_algorithm_.process(this->voc_sraw_);
136 if (this->nox_sensor_ != nullptr)
137 this->nox_index_ = this->nox_algorithm_.process(this->nox_sraw_);
138 ESP_LOGV(TAG, "VOC: %" PRId32 ", NOx: %" PRId32, this->voc_index_, this->nox_index_);
139 // Store baselines after defined interval or if the difference between current and stored baseline becomes too
140 // much
142 this->voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_);
143 if (std::abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF_STATE0 ||
144 std::abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF_STATE1) {
148
149 if (this->pref_.save(&this->voc_baselines_storage_)) {
150 ESP_LOGV(TAG, "Stored VOC baseline state0: %f, state1: %f", this->voc_baselines_storage_.state0,
151 this->voc_baselines_storage_.state1);
152 } else {
153 ESP_LOGW(TAG, "Storing VOC baselines failed");
154 }
155 }
156 }
157
159 this->samples_read_++;
160 ESP_LOGD(TAG, "Stabilizing (%d/%d); VOC index: %" PRIu32, this->samples_read_, this->samples_to_stabilize_,
161 this->voc_index_);
162 }
163}
164
166 float humidity = NAN;
167
168 if (!this->self_test_complete_) {
169 ESP_LOGW(TAG, "Self-test incomplete");
170 return;
171 }
172 if (this->humidity_sensor_ != nullptr) {
173 humidity = this->humidity_sensor_->state;
174 }
175 if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
176 humidity = 50;
177 }
178
179 float temperature = NAN;
180 if (this->temperature_sensor_ != nullptr) {
181 temperature = float(this->temperature_sensor_->state);
182 }
183 if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
184 temperature = 25;
185 }
186
187 uint16_t command;
188 uint16_t data[2];
189 size_t response_words;
190 // Use SGP40 measure command if we don't care about NOx
191 if (nox_sensor_ == nullptr) {
192 command = SGP40_CMD_MEASURE_RAW;
193 response_words = 1;
194 } else {
195 // SGP41 sensor must use NOx conditioning command for the first 10 seconds
196 if (this->nox_conditioning_start_.has_value() && millis() - *this->nox_conditioning_start_ < 10000) {
197 command = SGP41_CMD_NOX_CONDITIONING;
198 response_words = 1;
199 } else {
200 this->nox_conditioning_start_.reset();
201 command = SGP41_CMD_MEASURE_RAW;
202 response_words = 2;
203 }
204 }
205 uint16_t rhticks = (uint16_t) std::llround((humidity * 65535) / 100);
206 uint16_t tempticks = (uint16_t) (((temperature + 45) * 65535) / 175);
207 // first parameter are the relative humidity ticks
208 data[0] = rhticks;
209 // secomd parameter are the temperature ticks
210 data[1] = tempticks;
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 switch (this->error_code_) {
264 case COMMUNICATION_FAILED:
265 ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
266 break;
267 case SERIAL_NUMBER_IDENTIFICATION_FAILED:
268 ESP_LOGW(TAG, "Get serial number failed");
269 break;
270 case SELF_TEST_FAILED:
271 ESP_LOGW(TAG, "Self-test failed");
272 break;
273 default:
274 ESP_LOGW(TAG, "Unknown error");
275 break;
276 }
277 } else {
278 ESP_LOGCONFIG(TAG,
279 " Type: %s\n"
280 " Serial number: %" PRIu64 "\n"
281 " Minimum Samples: %f",
282 sgp_type_ == SGP41 ? "SGP41" : "SPG40", this->serial_number_, GasIndexAlgorithm_INITIAL_BLACKOUT);
283 }
284 LOG_UPDATE_INTERVAL(this);
285
286 ESP_LOGCONFIG(TAG, " Compensation:");
287 if (this->humidity_sensor_ != nullptr || this->temperature_sensor_ != nullptr) {
288 LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_);
289 LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_);
290 } else {
291 ESP_LOGCONFIG(TAG, " No source configured");
292 }
293 LOG_SENSOR(" ", "VOC", this->voc_sensor_);
294 LOG_SENSOR(" ", "NOx", this->nox_sensor_);
295}
296
297} // 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:140
optional< uint32_t > nox_conditioning_start_
Definition sgp4x.h:137
ESPPreferenceObject pref_
Definition sgp4x.h:138
void dump_config() override
Definition sgp4x.cpp:257
sensor::Sensor * humidity_sensor_
Input sensor for humidity and temperature compensation.
Definition sgp4x.h:105
sensor::Sensor * voc_sensor_
Definition sgp4x.h:120
VOCGasIndexAlgorithm voc_algorithm_
Definition sgp4x.h:121
sensor::Sensor * temperature_sensor_
Definition sgp4x.h:106
optional< GasTuning > voc_tuning_params_
Definition sgp4x.h:122
NOxGasIndexAlgorithm nox_algorithm_
Definition sgp4x.h:129
optional< GasTuning > nox_tuning_params_
Definition sgp4x.h:130
sensor::Sensor * nox_sensor_
Definition sgp4x.h:127
const float MAXIMUM_STORAGE_DIFF_STATE0
Definition sgp4x.h:53
const float MAXIMUM_STORAGE_DIFF_STATE1
Definition sgp4x.h:56
const uint32_t SHORTEST_BASELINE_STORE_INTERVAL
Definition sgp4x.h:46
constexpr uint32_t fnv1a_hash_extend(uint32_t hash, const char *str)
Extend a FNV-1a hash with additional string data.
Definition helpers.h:825
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