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