ESPHome  2022.9.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
65  // This ensures the baseline storage is cleared after OTA
66  uint32_t hash = fnv1_hash(App.get_compilation_time());
68 
69  if (this->pref_.load(&this->voc_baselines_storage_)) {
72  ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->voc_baselines_storage_.state0,
74  }
75 
76  // Initialize storage timestamp
77  this->seconds_since_last_store_ = 0;
78 
79  if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) {
80  ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X",
82  voc_algorithm_.set_states(this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
83  }
84  }
85  if (this->voc_sensor_ && this->voc_tuning_params_.has_value()) {
86  voc_algorithm_.set_tuning_parameters(
87  voc_tuning_params_.value().index_offset, voc_tuning_params_.value().learning_time_offset_hours,
88  voc_tuning_params_.value().learning_time_gain_hours, voc_tuning_params_.value().gating_max_duration_minutes,
89  voc_tuning_params_.value().std_initial, voc_tuning_params_.value().gain_factor);
90  }
91 
92  if (this->nox_sensor_ && this->nox_tuning_params_.has_value()) {
93  nox_algorithm_.set_tuning_parameters(
94  nox_tuning_params_.value().index_offset, nox_tuning_params_.value().learning_time_offset_hours,
95  nox_tuning_params_.value().learning_time_gain_hours, nox_tuning_params_.value().gating_max_duration_minutes,
96  nox_tuning_params_.value().std_initial, nox_tuning_params_.value().gain_factor);
97  }
98 
99  this->self_test_();
100 
101  /* The official spec for this sensor at
102  https://sensirion.com/media/documents/296373BB/6203C5DF/Sensirion_Gas_Sensors_Datasheet_SGP40.pdf indicates this
103  sensor should be driven at 1Hz. Comments from the developers at:
104  https://github.com/Sensirion/embedded-sgp/issues/136 indicate the algorithm should be a bit resilient to slight
105  timing variations so the software timer should be accurate enough for this.
106 
107  This block starts sampling from the sensor at 1Hz, and is done separately from the call
108  to the update method. This separation is to support getting accurate measurements but
109  limit the amount of communication done over wifi for power consumption or to keep the
110  number of records reported from being overwhelming.
111  */
112  ESP_LOGD(TAG, "Component requires sampling of 1Hz, setting up background sampler");
113  this->set_interval(1000, [this]() { this->update_gas_indices(); });
114 }
115 
117  ESP_LOGD(TAG, "Self-test started");
118  if (!this->write_command(SGP4X_CMD_SELF_TEST)) {
119  this->error_code_ = COMMUNICATION_FAILED;
120  ESP_LOGD(TAG, "Self-test communication failed");
121  this->mark_failed();
122  }
123 
124  this->set_timeout(self_test_time_, [this]() {
125  uint16_t reply;
126  if (!this->read_data(reply)) {
127  this->error_code_ = SELF_TEST_FAILED;
128  ESP_LOGD(TAG, "Self-test read_data_ failed");
129  this->mark_failed();
130  return;
131  }
132 
133  if (reply == 0xD400) {
134  this->self_test_complete_ = true;
135  ESP_LOGD(TAG, "Self-test completed");
136  return;
137  } else {
138  this->error_code_ = SELF_TEST_FAILED;
139  ESP_LOGD(TAG, "Self-test failed 0x%X", reply);
140  return;
141  }
142 
143  ESP_LOGD(TAG, "Self-test failed 0x%X", reply);
144  this->mark_failed();
145  });
146 }
147 
156 bool SGP4xComponent::measure_gas_indices_(int32_t &voc, int32_t &nox) {
157  uint16_t voc_sraw;
158  uint16_t nox_sraw;
159  if (!measure_raw_(voc_sraw, nox_sraw))
160  return false;
161 
162  this->status_clear_warning();
163 
164  voc = voc_algorithm_.process(voc_sraw);
165  if (nox_sensor_) {
166  nox = nox_algorithm_.process(nox_sraw);
167  }
168  ESP_LOGV(TAG, "VOC = %d, NOx = %d", voc, nox);
169  // Store baselines after defined interval or if the difference between current and stored baseline becomes too
170  // much
172  voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_);
173  if (std::abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF ||
174  std::abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF) {
175  this->seconds_since_last_store_ = 0;
178 
179  if (this->pref_.save(&this->voc_baselines_storage_)) {
180  ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->voc_baselines_storage_.state0,
182  } else {
183  ESP_LOGW(TAG, "Could not store VOC baselines");
184  }
185  }
186  }
187 
188  return true;
189 }
197 bool SGP4xComponent::measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw) {
198  float humidity = NAN;
199  static uint32_t nox_conditioning_start = millis();
200 
201  if (!this->self_test_complete_) {
202  ESP_LOGD(TAG, "Self-test not yet complete");
203  return false;
204  }
205  if (this->humidity_sensor_ != nullptr) {
206  humidity = this->humidity_sensor_->state;
207  }
208  if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
209  humidity = 50;
210  }
211 
212  float temperature = NAN;
213  if (this->temperature_sensor_ != nullptr) {
214  temperature = float(this->temperature_sensor_->state);
215  }
216  if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
217  temperature = 25;
218  }
219 
220  uint16_t command;
221  uint16_t data[2];
222  size_t response_words;
223  // Use SGP40 measure command if we don't care about NOx
224  if (nox_sensor_ == nullptr) {
225  command = SGP40_CMD_MEASURE_RAW;
226  response_words = 1;
227  } else {
228  // SGP41 sensor must use NOx conditioning command for the first 10 seconds
229  if (millis() - nox_conditioning_start < 10000) {
230  command = SGP41_CMD_NOX_CONDITIONING;
231  response_words = 1;
232  } else {
233  command = SGP41_CMD_MEASURE_RAW;
234  response_words = 2;
235  }
236  }
237  uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100));
238  uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175);
239  // first parameter are the relative humidity ticks
240  data[0] = rhticks;
241  // secomd parameter are the temperature ticks
242  data[1] = tempticks;
243 
244  if (!this->write_command(command, data, 2)) {
245  this->status_set_warning();
246  ESP_LOGD(TAG, "write error (%d)", this->last_error_);
247  return false;
248  }
250  uint16_t raw_data[2];
251  raw_data[1] = 0;
252  if (!this->read_data(raw_data, response_words)) {
253  this->status_set_warning();
254  ESP_LOGD(TAG, "read error (%d)", this->last_error_);
255  return false;
256  }
257  voc_raw = raw_data[0];
258  nox_raw = raw_data[1]; // either 0 or the measured NOx ticks
259  return true;
260 }
261 
263  if (!this->self_test_complete_)
264  return;
265 
266  this->seconds_since_last_store_ += 1;
267  if (!this->measure_gas_indices_(this->voc_index_, this->nox_index_)) {
268  // Set values to UINT16_MAX to indicate failure
269  this->voc_index_ = this->nox_index_ = UINT16_MAX;
270  ESP_LOGE(TAG, "measure gas indices failed");
271  return;
272  }
273  if (this->samples_read_ < this->samples_to_stabilize_) {
274  this->samples_read_++;
275  ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %u", this->samples_read_,
276  this->samples_to_stabilize_, this->voc_index_);
277  return;
278  }
279 }
280 
282  if (this->samples_read_ < this->samples_to_stabilize_) {
283  return;
284  }
285  if (this->voc_sensor_) {
286  if (this->voc_index_ != UINT16_MAX) {
287  this->status_clear_warning();
288  this->voc_sensor_->publish_state(this->voc_index_);
289  } else {
290  this->status_set_warning();
291  }
292  }
293  if (this->nox_sensor_) {
294  if (this->nox_index_ != UINT16_MAX) {
295  this->status_clear_warning();
296  this->nox_sensor_->publish_state(this->nox_index_);
297  } else {
298  this->status_set_warning();
299  }
300  }
301 }
302 
304  ESP_LOGCONFIG(TAG, "SGP4x:");
305  LOG_I2C_DEVICE(this);
306  ESP_LOGCONFIG(TAG, " store_baseline: %d", this->store_baseline_);
307 
308  if (this->is_failed()) {
309  switch (this->error_code_) {
310  case COMMUNICATION_FAILED:
311  ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
312  break;
313  case SERIAL_NUMBER_IDENTIFICATION_FAILED:
314  ESP_LOGW(TAG, "Get Serial number failed.");
315  break;
316  case SELF_TEST_FAILED:
317  ESP_LOGW(TAG, "Self test failed.");
318  break;
319 
320  default:
321  ESP_LOGW(TAG, "Unknown setup error!");
322  break;
323  }
324  } else {
325  ESP_LOGCONFIG(TAG, " Type: %s", sgp_type_ == SGP41 ? "SGP41" : "SPG40");
326  ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_);
327  ESP_LOGCONFIG(TAG, " Minimum Samples: %f", GasIndexAlgorithm_INITIAL_BLACKOUT);
328  }
329  LOG_UPDATE_INTERVAL(this);
330 
331  if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) {
332  ESP_LOGCONFIG(TAG, " Compensation:");
333  LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_);
334  LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_);
335  } else {
336  ESP_LOGCONFIG(TAG, " Compensation: No source configured");
337  }
338  LOG_SENSOR(" ", "VOC", this->voc_sensor_);
339  LOG_SENSOR(" ", "NOx", this->nox_sensor_);
340 }
341 
342 } // namespace sgp4x
343 } // namespace esphome
void set_disabled_by_default(bool disabled_by_default)
Definition: entity_base.cpp:23
sensor::Sensor * voc_sensor_
Definition: sgp4x.h:120
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:50
void set_internal(bool internal)
Definition: entity_base.cpp:19
const float MAXIMUM_STORAGE_DIFF
Definition: sgp4x.h:52
bool write_command(T i2c_register)
Write a command to the i2c device.
Definition: i2c_sensirion.h:80
void dump_config() override
Definition: sgp4x.cpp:303
const uint32_t SHORTEST_BASELINE_STORE_INTERVAL
Definition: sgp4x.h:46
VOCGasIndexAlgorithm voc_algorithm_
Definition: sgp4x.h:121
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:67
float temperature
Definition: qmp6988.h:71
optional< GasTuning > nox_tuning_params_
Definition: sgp4x.h:130
ESPPreferenceObject pref_
Definition: sgp4x.h:137
bool read_data(uint16_t *data, uint8_t len)
Read data words from i2c device.
sensor::Sensor * nox_sensor_
Definition: sgp4x.h:127
sensor::Sensor * humidity_sensor_
Input sensor for humidity and temperature compensation.
Definition: sgp4x.h:106
uint32_t IRAM_ATTR HOT millis()
Definition: core.cpp:26
bool save(const T *src)
Definition: preferences.h:21
optional< GasTuning > voc_tuning_params_
Definition: sgp4x.h:122
float state
This member variable stores the last state that has passed through all filters.
Definition: sensor.h:133
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:156
ESPPreferences * global_preferences
void status_clear_warning()
Definition: component.cpp:148
uint32_t seconds_since_last_store_
Definition: sgp4x.h:138
void publish_state(float state)
Publish a new state to the front-end.
Definition: sensor.cpp:72
SGP4xBaselines voc_baselines_storage_
Definition: sgp4x.h:139
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:197
NOxGasIndexAlgorithm nox_algorithm_
Definition: sgp4x.h:129
void status_set_warning()
Definition: component.cpp:140
const std::string & get_compilation_time() const
Definition: application.h:139
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:41
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:80
virtual void mark_failed()
Mark this component as failed.
Definition: component.cpp:111
i2c::ErrorCode last_error_
last error code from i2c operation
sensor::Sensor * temperature_sensor_
Definition: sgp4x.h:107
Definition: a4988.cpp:4
void setup() override
Definition: sgp4x.cpp:11
void update() override
Definition: sgp4x.cpp:281
void IRAM_ATTR HOT delay(uint32_t ms)
Definition: core.cpp:27