ESPHome  2024.3.1
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 
6 namespace esphome {
7 namespace sgp4x {
8 
9 static 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) {
35  sgp_type_ = SGP40;
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) {
50  sgp_type_ = SGP41;
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
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
78  this->seconds_since_last_store_ = 0;
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->update_gas_indices(); });
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 
157 bool SGP4xComponent::measure_gas_indices_(int32_t &voc, int32_t &nox) {
158  uint16_t voc_sraw;
159  uint16_t nox_sraw;
160  if (!measure_raw_(voc_sraw, nox_sraw))
161  return false;
162 
163  this->status_clear_warning();
164 
165  voc = voc_algorithm_.process(voc_sraw);
166  if (nox_sensor_) {
167  nox = nox_algorithm_.process(nox_sraw);
168  }
169  ESP_LOGV(TAG, "VOC = %" PRId32 ", NOx = %" PRId32, voc, nox);
170  // Store baselines after defined interval or if the difference between current and stored baseline becomes too
171  // much
173  voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_);
174  if (std::abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF ||
175  std::abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF) {
176  this->seconds_since_last_store_ = 0;
179 
180  if (this->pref_.save(&this->voc_baselines_storage_)) {
181  ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 " ,state1: 0x%04" PRIX32,
183  } else {
184  ESP_LOGW(TAG, "Could not store VOC baselines");
185  }
186  }
187  }
188 
189  return true;
190 }
198 bool SGP4xComponent::measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw) {
199  float humidity = NAN;
200  static uint32_t nox_conditioning_start = millis();
201 
202  if (!this->self_test_complete_) {
203  ESP_LOGD(TAG, "Self-test not yet complete");
204  return false;
205  }
206  if (this->humidity_sensor_ != nullptr) {
207  humidity = this->humidity_sensor_->state;
208  }
209  if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
210  humidity = 50;
211  }
212 
213  float temperature = NAN;
214  if (this->temperature_sensor_ != nullptr) {
215  temperature = float(this->temperature_sensor_->state);
216  }
217  if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
218  temperature = 25;
219  }
220 
221  uint16_t command;
222  uint16_t data[2];
223  size_t response_words;
224  // Use SGP40 measure command if we don't care about NOx
225  if (nox_sensor_ == nullptr) {
226  command = SGP40_CMD_MEASURE_RAW;
227  response_words = 1;
228  } else {
229  // SGP41 sensor must use NOx conditioning command for the first 10 seconds
230  if (millis() - nox_conditioning_start < 10000) {
231  command = SGP41_CMD_NOX_CONDITIONING;
232  response_words = 1;
233  } else {
234  command = SGP41_CMD_MEASURE_RAW;
235  response_words = 2;
236  }
237  }
238  uint16_t rhticks = llround((uint16_t) ((humidity * 65535) / 100));
239  uint16_t tempticks = (uint16_t) (((temperature + 45) * 65535) / 175);
240  // first parameter are the relative humidity ticks
241  data[0] = rhticks;
242  // secomd parameter are the temperature ticks
243  data[1] = tempticks;
244 
245  if (!this->write_command(command, data, 2)) {
246  this->status_set_warning();
247  ESP_LOGD(TAG, "write error (%d)", this->last_error_);
248  return false;
249  }
251  uint16_t raw_data[2];
252  raw_data[1] = 0;
253  if (!this->read_data(raw_data, response_words)) {
254  this->status_set_warning();
255  ESP_LOGD(TAG, "read error (%d)", this->last_error_);
256  return false;
257  }
258  voc_raw = raw_data[0];
259  nox_raw = raw_data[1]; // either 0 or the measured NOx ticks
260  return true;
261 }
262 
264  if (!this->self_test_complete_)
265  return;
266 
267  this->seconds_since_last_store_ += 1;
268  if (!this->measure_gas_indices_(this->voc_index_, this->nox_index_)) {
269  // Set values to UINT16_MAX to indicate failure
270  this->voc_index_ = this->nox_index_ = UINT16_MAX;
271  ESP_LOGE(TAG, "measure gas indices failed");
272  return;
273  }
274  if (this->samples_read_ < this->samples_to_stabilize_) {
275  this->samples_read_++;
276  ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %" PRIu32, this->samples_read_,
277  this->samples_to_stabilize_, this->voc_index_);
278  return;
279  }
280 }
281 
283  if (this->samples_read_ < this->samples_to_stabilize_) {
284  return;
285  }
286  if (this->voc_sensor_) {
287  if (this->voc_index_ != UINT16_MAX) {
288  this->status_clear_warning();
289  this->voc_sensor_->publish_state(this->voc_index_);
290  } else {
291  this->status_set_warning();
292  }
293  }
294  if (this->nox_sensor_) {
295  if (this->nox_index_ != UINT16_MAX) {
296  this->status_clear_warning();
297  this->nox_sensor_->publish_state(this->nox_index_);
298  } else {
299  this->status_set_warning();
300  }
301  }
302 }
303 
305  ESP_LOGCONFIG(TAG, "SGP4x:");
306  LOG_I2C_DEVICE(this);
307  ESP_LOGCONFIG(TAG, " store_baseline: %d", this->store_baseline_);
308 
309  if (this->is_failed()) {
310  switch (this->error_code_) {
311  case COMMUNICATION_FAILED:
312  ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
313  break;
314  case SERIAL_NUMBER_IDENTIFICATION_FAILED:
315  ESP_LOGW(TAG, "Get Serial number failed.");
316  break;
317  case SELF_TEST_FAILED:
318  ESP_LOGW(TAG, "Self test failed.");
319  break;
320 
321  default:
322  ESP_LOGW(TAG, "Unknown setup error!");
323  break;
324  }
325  } else {
326  ESP_LOGCONFIG(TAG, " Type: %s", sgp_type_ == SGP41 ? "SGP41" : "SPG40");
327  ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_);
328  ESP_LOGCONFIG(TAG, " Minimum Samples: %f", GasIndexAlgorithm_INITIAL_BLACKOUT);
329  }
330  LOG_UPDATE_INTERVAL(this);
331 
332  if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) {
333  ESP_LOGCONFIG(TAG, " Compensation:");
334  LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_);
335  LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_);
336  } else {
337  ESP_LOGCONFIG(TAG, " Compensation: No source configured");
338  }
339  LOG_SENSOR(" ", "VOC", this->voc_sensor_);
340  LOG_SENSOR(" ", "NOx", this->nox_sensor_);
341 }
342 
343 } // namespace sgp4x
344 } // namespace esphome
void set_disabled_by_default(bool disabled_by_default)
Definition: entity_base.cpp:27
sensor::Sensor * voc_sensor_
Definition: sgp4x.h:121
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:52
void set_internal(bool internal)
Definition: entity_base.cpp:23
const char * to_string(SHTCXType type)
Definition: shtcx.cpp:16
const float MAXIMUM_STORAGE_DIFF
Definition: sgp4x.h:53
void status_set_warning(const char *message="unspecified")
Definition: component.cpp:146
bool write_command(T i2c_register)
Write a command to the i2c device.
Definition: i2c_sensirion.h:82
void dump_config() override
Definition: sgp4x.cpp:304
const uint32_t SHORTEST_BASELINE_STORE_INTERVAL
Definition: sgp4x.h:47
VOCGasIndexAlgorithm voc_algorithm_
Definition: sgp4x.h:122
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:69
float temperature
Definition: qmp6988.h:71
optional< GasTuning > nox_tuning_params_
Definition: sgp4x.h:131
ESPPreferenceObject pref_
Definition: sgp4x.h:138
bool read_data(uint16_t *data, uint8_t len)
Read data words from i2c device.
sensor::Sensor * nox_sensor_
Definition: sgp4x.h:128
sensor::Sensor * humidity_sensor_
Input sensor for humidity and temperature compensation.
Definition: sgp4x.h:107
uint32_t IRAM_ATTR HOT millis()
Definition: core.cpp:25
bool save(const T *src)
Definition: preferences.h:21
optional< GasTuning > voc_tuning_params_
Definition: sgp4x.h:123
float state
This member variable stores the last state that has passed through all filters.
Definition: sensor.h:131
bool measure_gas_indices_(int32_t &voc, int32_t &nox)
Combined the measured gasses, temperature, and humidity to calculate the VOC Index.
Definition: sgp4x.cpp:157
ESPPreferences * global_preferences
void status_clear_warning()
Definition: component.cpp:161
uint32_t seconds_since_last_store_
Definition: sgp4x.h:139
void publish_state(float state)
Publish a new state to the front-end.
Definition: sensor.cpp:39
SGP4xBaselines voc_baselines_storage_
Definition: sgp4x.h:140
Application App
Global storage of Application pointer - only one Application can exist.
bool measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw)
Return the raw gas measurement.
Definition: sgp4x.cpp:198
NOxGasIndexAlgorithm nox_algorithm_
Definition: sgp4x.h:130
bool get_register(uint16_t command, uint16_t *data, uint8_t len, uint8_t delay=0)
get data words from i2c register.
Definition: i2c_sensirion.h:43
virtual ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash)=0
uint32_t fnv1_hash(const std::string &str)
Calculate a FNV-1 hash of str.
Definition: helpers.cpp:184
virtual void mark_failed()
Mark this component as failed.
Definition: component.cpp:113
i2c::ErrorCode last_error_
last error code from i2c operation
sensor::Sensor * temperature_sensor_
Definition: sgp4x.h:108
This is a workaround until we can figure out a way to get the tflite-micro idf component code availab...
Definition: a01nyub.cpp:7
void setup() override
Definition: sgp4x.cpp:11
void update() override
Definition: sgp4x.cpp:282
std::string get_compilation_time() const
Definition: application.h:180
void IRAM_ATTR HOT delay(uint32_t ms)
Definition: core.cpp:26