ESPHome 2025.5.0
Loading...
Searching...
No Matches
sgp4x.cpp
Go to the documentation of this file.
1#include "sgp4x.h"
2#include "esphome/core/log.h"
3#include "esphome/core/hal.h"
4#include <cinttypes>
5
6namespace esphome {
7namespace sgp4x {
8
9static const char *const TAG = "sgp4x";
10
12 ESP_LOGCONFIG(TAG, "Setting up SGP4x...");
13
14 // Serial Number identification
15 uint16_t raw_serial_number[3];
16 if (!this->get_register(SGP4X_CMD_GET_SERIAL_ID, raw_serial_number, 3, 1)) {
17 ESP_LOGE(TAG, "Failed to read serial number");
18 this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED;
19 this->mark_failed();
20 return;
21 }
22 this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) |
23 (uint64_t(raw_serial_number[2]));
24 ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_);
25
26 // Featureset identification for future use
27 uint16_t raw_featureset;
28 if (!this->get_register(SGP4X_CMD_GET_FEATURESET, raw_featureset, 1)) {
29 ESP_LOGD(TAG, "raw_featureset write_command_ failed");
30 this->mark_failed();
31 return;
32 }
33 this->featureset_ = raw_featureset;
34 if ((this->featureset_ & 0x1FF) == SGP40_FEATURESET) {
36 self_test_time_ = SPG40_SELFTEST_TIME;
37 measure_time_ = SGP40_MEASURE_TIME;
38 if (this->nox_sensor_) {
39 ESP_LOGE(TAG, "Measuring NOx requires a SGP41 sensor but a SGP40 sensor is detected");
40 // disable the sensor
42 // make sure it's not visible in HA
43 this->nox_sensor_->set_internal(true);
44 this->nox_sensor_->state = NAN;
45 // remove pointer to sensor
46 this->nox_sensor_ = nullptr;
47 }
48 } else {
49 if ((this->featureset_ & 0x1FF) == SGP41_FEATURESET) {
51 self_test_time_ = SPG41_SELFTEST_TIME;
52 measure_time_ = SGP41_MEASURE_TIME;
53 } else {
54 ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF),
55 SGP40_FEATURESET);
56 this->mark_failed();
57 return;
58 }
59 }
60
61 ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF));
62
63 if (this->store_baseline_) {
64 // Hash with compilation time and serial number
65 // This ensures the baseline storage is cleared after OTA
66 // Serial numbers are unique to each sensor, so mulitple sensors can be used without conflict
67 uint32_t hash = fnv1_hash(App.get_compilation_time() + std::to_string(this->serial_number_));
69
70 if (this->pref_.load(&this->voc_baselines_storage_)) {
73 ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
75 }
76
77 // Initialize storage timestamp
79
80 if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) {
81 ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
83 voc_algorithm_.set_states(this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
84 }
85 }
86 if (this->voc_sensor_ && this->voc_tuning_params_.has_value()) {
87 voc_algorithm_.set_tuning_parameters(
88 voc_tuning_params_.value().index_offset, voc_tuning_params_.value().learning_time_offset_hours,
89 voc_tuning_params_.value().learning_time_gain_hours, voc_tuning_params_.value().gating_max_duration_minutes,
90 voc_tuning_params_.value().std_initial, voc_tuning_params_.value().gain_factor);
91 }
92
93 if (this->nox_sensor_ && this->nox_tuning_params_.has_value()) {
94 nox_algorithm_.set_tuning_parameters(
95 nox_tuning_params_.value().index_offset, nox_tuning_params_.value().learning_time_offset_hours,
96 nox_tuning_params_.value().learning_time_gain_hours, nox_tuning_params_.value().gating_max_duration_minutes,
97 nox_tuning_params_.value().std_initial, nox_tuning_params_.value().gain_factor);
98 }
99
100 this->self_test_();
101
102 /* The official spec for this sensor at
103 https://sensirion.com/media/documents/296373BB/6203C5DF/Sensirion_Gas_Sensors_Datasheet_SGP40.pdf indicates this
104 sensor should be driven at 1Hz. Comments from the developers at:
105 https://github.com/Sensirion/embedded-sgp/issues/136 indicate the algorithm should be a bit resilient to slight
106 timing variations so the software timer should be accurate enough for this.
107
108 This block starts sampling from the sensor at 1Hz, and is done separately from the call
109 to the update method. This separation is to support getting accurate measurements but
110 limit the amount of communication done over wifi for power consumption or to keep the
111 number of records reported from being overwhelming.
112 */
113 ESP_LOGD(TAG, "Component requires sampling of 1Hz, setting up background sampler");
114 this->set_interval(1000, [this]() { this->take_sample(); });
115}
116
118 ESP_LOGD(TAG, "Self-test started");
119 if (!this->write_command(SGP4X_CMD_SELF_TEST)) {
120 this->error_code_ = COMMUNICATION_FAILED;
121 ESP_LOGD(TAG, "Self-test communication failed");
122 this->mark_failed();
123 }
124
125 this->set_timeout(self_test_time_, [this]() {
126 uint16_t reply;
127 if (!this->read_data(reply)) {
128 this->error_code_ = SELF_TEST_FAILED;
129 ESP_LOGD(TAG, "Self-test read_data_ failed");
130 this->mark_failed();
131 return;
132 }
133
134 if (reply == 0xD400) {
135 this->self_test_complete_ = true;
136 ESP_LOGD(TAG, "Self-test completed");
137 return;
138 } else {
139 this->error_code_ = SELF_TEST_FAILED;
140 ESP_LOGD(TAG, "Self-test failed 0x%X", reply);
141 return;
142 }
143
144 ESP_LOGD(TAG, "Self-test failed 0x%X", reply);
145 this->mark_failed();
146 });
147}
148
150 this->voc_index_ = this->voc_algorithm_.process(this->voc_sraw_);
151 if (this->nox_sensor_ != nullptr)
152 this->nox_index_ = this->nox_algorithm_.process(this->nox_sraw_);
153 ESP_LOGV(TAG, "VOC = %" PRId32 ", NOx = %" PRId32, this->voc_index_, this->nox_index_);
154 // Store baselines after defined interval or if the difference between current and stored baseline becomes too
155 // much
157 this->voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_);
158 if (std::abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF ||
159 std::abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF) {
163
164 if (this->pref_.save(&this->voc_baselines_storage_)) {
165 ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 " ,state1: 0x%04" PRIX32,
166 this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
167 } else {
168 ESP_LOGW(TAG, "Could not store VOC baselines");
169 }
170 }
171 }
172
174 this->samples_read_++;
175 ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %" PRIu32, this->samples_read_,
176 this->samples_to_stabilize_, this->voc_index_);
177 }
178}
179
181 float humidity = NAN;
182 static uint32_t nox_conditioning_start = millis();
183
184 if (!this->self_test_complete_) {
185 ESP_LOGD(TAG, "Self-test not yet complete");
186 return;
187 }
188 if (this->humidity_sensor_ != nullptr) {
189 humidity = this->humidity_sensor_->state;
190 }
191 if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
192 humidity = 50;
193 }
194
195 float temperature = NAN;
196 if (this->temperature_sensor_ != nullptr) {
197 temperature = float(this->temperature_sensor_->state);
198 }
199 if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
200 temperature = 25;
201 }
202
203 uint16_t command;
204 uint16_t data[2];
205 size_t response_words;
206 // Use SGP40 measure command if we don't care about NOx
207 if (nox_sensor_ == nullptr) {
208 command = SGP40_CMD_MEASURE_RAW;
209 response_words = 1;
210 } else {
211 // SGP41 sensor must use NOx conditioning command for the first 10 seconds
212 if (millis() - nox_conditioning_start < 10000) {
213 command = SGP41_CMD_NOX_CONDITIONING;
214 response_words = 1;
215 } else {
216 command = SGP41_CMD_MEASURE_RAW;
217 response_words = 2;
218 }
219 }
220 uint16_t rhticks = llround((uint16_t) ((humidity * 65535) / 100));
221 uint16_t tempticks = (uint16_t) (((temperature + 45) * 65535) / 175);
222 // first parameter are the relative humidity ticks
223 data[0] = rhticks;
224 // secomd parameter are the temperature ticks
225 data[1] = tempticks;
226
227 if (!this->write_command(command, data, 2)) {
228 ESP_LOGD(TAG, "write error (%d)", this->last_error_);
229 this->status_set_warning("measurement request failed");
230 return;
231 }
232
233 this->set_timeout(this->measure_time_, [this, response_words]() {
234 uint16_t raw_data[2];
235 raw_data[1] = 0;
236 if (!this->read_data(raw_data, response_words)) {
237 ESP_LOGD(TAG, "read error (%d)", this->last_error_);
238 this->status_set_warning("measurement read failed");
239 this->voc_index_ = this->nox_index_ = UINT16_MAX;
240 return;
241 }
242 this->voc_sraw_ = raw_data[0];
243 this->nox_sraw_ = raw_data[1]; // either 0 or the measured NOx ticks
244 this->status_clear_warning();
245 this->update_gas_indices_();
246 });
247}
248
250 if (!this->self_test_complete_)
251 return;
252 this->seconds_since_last_store_ += 1;
253 this->measure_raw_();
254}
255
258 return;
259 }
260 if (this->voc_sensor_ != nullptr) {
261 if (this->voc_index_ != UINT16_MAX)
263 }
264 if (this->nox_sensor_ != nullptr) {
265 if (this->nox_index_ != UINT16_MAX)
267 }
268}
269
271 ESP_LOGCONFIG(TAG, "SGP4x:");
272 LOG_I2C_DEVICE(this);
273 ESP_LOGCONFIG(TAG, " store_baseline: %d", this->store_baseline_);
274
275 if (this->is_failed()) {
276 switch (this->error_code_) {
277 case COMMUNICATION_FAILED:
278 ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
279 break;
280 case SERIAL_NUMBER_IDENTIFICATION_FAILED:
281 ESP_LOGW(TAG, "Get Serial number failed.");
282 break;
283 case SELF_TEST_FAILED:
284 ESP_LOGW(TAG, "Self test failed.");
285 break;
286
287 default:
288 ESP_LOGW(TAG, "Unknown setup error!");
289 break;
290 }
291 } else {
292 ESP_LOGCONFIG(TAG, " Type: %s", sgp_type_ == SGP41 ? "SGP41" : "SPG40");
293 ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_);
294 ESP_LOGCONFIG(TAG, " Minimum Samples: %f", GasIndexAlgorithm_INITIAL_BLACKOUT);
295 }
296 LOG_UPDATE_INTERVAL(this);
297
298 if (this->humidity_sensor_ != nullptr || this->temperature_sensor_ != nullptr) {
299 ESP_LOGCONFIG(TAG, " Compensation:");
300 LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_);
301 LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_);
302 } else {
303 ESP_LOGCONFIG(TAG, " Compensation: No source configured");
304 }
305 LOG_SENSOR(" ", "VOC", this->voc_sensor_);
306 LOG_SENSOR(" ", "NOx", this->nox_sensor_);
307}
308
309} // namespace sgp4x
310} // namespace esphome
std::string get_compilation_time() const
virtual void mark_failed()
Mark this component as failed.
bool is_failed() const
void set_interval(const std::string &name, uint32_t interval, std::function< void()> &&f)
Set an interval function with a unique name.
Definition component.cpp:55
void status_set_warning(const char *message="unspecified")
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.
Definition component.cpp:72
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)
void set_internal(bool internal)
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:39
float state
This member variable stores the last state that has passed through all filters.
Definition sensor.h:131
SGP4xBaselines voc_baselines_storage_
Definition sgp4x.h:142
ESPPreferenceObject pref_
Definition sgp4x.h:140
void dump_config() override
Definition sgp4x.cpp:270
sensor::Sensor * humidity_sensor_
Input sensor for humidity and temperature compensation.
Definition sgp4x.h:107
sensor::Sensor * voc_sensor_
Definition sgp4x.h:123
VOCGasIndexAlgorithm voc_algorithm_
Definition sgp4x.h:124
sensor::Sensor * temperature_sensor_
Definition sgp4x.h:108
optional< GasTuning > voc_tuning_params_
Definition sgp4x.h:125
NOxGasIndexAlgorithm nox_algorithm_
Definition sgp4x.h:132
optional< GasTuning > nox_tuning_params_
Definition sgp4x.h:133
sensor::Sensor * nox_sensor_
Definition sgp4x.h:130
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
uint32_t fnv1_hash(const std::string &str)
Calculate a FNV-1 hash of str.
Definition helpers.cpp:186
ESPPreferences * global_preferences
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:27
Application App
Global storage of Application pointer - only one Application can exist.
uint16_t temperature
Definition sun_gtil2.cpp:12