ESPHome  2024.12.2
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->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) {
160  this->seconds_since_last_store_ = 0;
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 
173  if (this->samples_read_ < this->samples_to_stabilize_) {
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 
257  if (this->samples_read_ < this->samples_to_stabilize_) {
258  return;
259  }
260  if (this->voc_sensor_ != nullptr) {
261  if (this->voc_index_ != UINT16_MAX)
262  this->voc_sensor_->publish_state(this->voc_index_);
263  }
264  if (this->nox_sensor_ != nullptr) {
265  if (this->nox_index_ != UINT16_MAX)
266  this->nox_sensor_->publish_state(this->nox_index_);
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
void set_disabled_by_default(bool disabled_by_default)
Definition: entity_base.cpp:27
sensor::Sensor * voc_sensor_
Definition: sgp4x.h:123
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:151
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:270
bool is_failed() const
Definition: component.cpp:143
const uint32_t SHORTEST_BASELINE_STORE_INTERVAL
Definition: sgp4x.h:47
VOCGasIndexAlgorithm voc_algorithm_
Definition: sgp4x.h:124
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
optional< GasTuning > nox_tuning_params_
Definition: sgp4x.h:133
ESPPreferenceObject pref_
Definition: sgp4x.h:140
bool read_data(uint16_t *data, uint8_t len)
Read data words from i2c device.
sensor::Sensor * nox_sensor_
Definition: sgp4x.h:130
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:125
float state
This member variable stores the last state that has passed through all filters.
Definition: sensor.h:131
ESPPreferences * global_preferences
void status_clear_warning()
Definition: component.cpp:166
uint32_t seconds_since_last_store_
Definition: sgp4x.h:141
void publish_state(float state)
Publish a new state to the front-end.
Definition: sensor.cpp:39
SGP4xBaselines voc_baselines_storage_
Definition: sgp4x.h:142
uint16_t temperature
Definition: sun_gtil2.cpp:26
Application App
Global storage of Application pointer - only one Application can exist.
NOxGasIndexAlgorithm nox_algorithm_
Definition: sgp4x.h:132
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:183
virtual void mark_failed()
Mark this component as failed.
Definition: component.cpp:118
i2c::ErrorCode last_error_
last error code from i2c operation
sensor::Sensor * temperature_sensor_
Definition: sgp4x.h:108
Implementation of SPI Controller mode.
Definition: a01nyub.cpp:7
void setup() override
Definition: sgp4x.cpp:11
void update() override
Definition: sgp4x.cpp:256
std::string get_compilation_time() const
Definition: application.h:215