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