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