ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
scd4x.cpp
Go to the documentation of this file.
1#include "scd4x.h"
2#include "esphome/core/hal.h"
3#include "esphome/core/log.h"
4
5namespace esphome::scd4x {
6
7ESPHOME_LOG_TAG(TAG, "scd4x");
8
9static const uint16_t SCD41_ID = 0x1408;
10static const uint16_t SCD40_ID = 0x440;
11static const uint16_t SCD4X_CMD_GET_SERIAL_NUMBER = 0x3682;
12static const uint16_t SCD4X_CMD_TEMPERATURE_OFFSET = 0x241d;
13static const uint16_t SCD4X_CMD_ALTITUDE_COMPENSATION = 0x2427;
14static const uint16_t SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION = 0xe000;
15static const uint16_t SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION = 0x2416;
16static const uint16_t SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS = 0x21b1;
17static const uint16_t SCD4X_CMD_START_LOW_POWER_CONTINUOUS_MEASUREMENTS = 0x21ac;
18static const uint16_t SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT = 0x219d; // SCD41 only
19static const uint16_t SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT_RHT_ONLY = 0x2196;
20static const uint16_t SCD4X_CMD_GET_DATA_READY_STATUS = 0xe4b8;
21static const uint16_t SCD4X_CMD_READ_MEASUREMENT = 0xec05;
22static const uint16_t SCD4X_CMD_PERFORM_FORCED_CALIBRATION = 0x362f;
23static const uint16_t SCD4X_CMD_STOP_MEASUREMENTS = 0x3f86;
24static const uint16_t SCD4X_CMD_FACTORY_RESET = 0x3632;
25static const uint16_t SCD4X_CMD_GET_FEATURESET = 0x202f;
26static const float SCD4X_TEMPERATURE_OFFSET_MULTIPLIER = (1 << 16) / 175.0f;
27
29 // the sensor needs 1000 ms to enter the idle state
30 this->set_timeout(1000, [this]() {
31 this->status_clear_error();
32 if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
33 this->mark_failed(LOG_STR("Failed to stop measurements"));
34 return;
35 }
36 // According to the SCD4x datasheet the sensor will only respond to other commands after waiting 500 ms after
37 // issuing the stop_periodic_measurement command
38 this->set_timeout(500, [this]() {
39 uint16_t raw_serial_number[3];
40 if (!this->get_register(SCD4X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 1)) {
41 this->mark_failed(LOG_STR("Unable to read serial number"));
42 return;
43 }
44 ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", (uint16_t(raw_serial_number[0]) >> 8),
45 uint16_t(raw_serial_number[0] & 0xFF), (uint16_t(raw_serial_number[1]) >> 8));
46
47 if (!this->write_command(SCD4X_CMD_TEMPERATURE_OFFSET,
48 (uint16_t) (temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) {
49 this->mark_failed(LOG_STR("Error setting temperature offset"));
50 return;
51 }
52
53 // If pressure compensation available use it, else use altitude
54 if (this->ambient_pressure_) {
56 this->mark_failed(LOG_STR("Error setting ambient pressure compensation"));
57 return;
58 }
59 } else {
60 if (!this->write_command(SCD4X_CMD_ALTITUDE_COMPENSATION, this->altitude_compensation_)) {
61 this->mark_failed(LOG_STR("Error setting altitude compensation"));
62 return;
63 }
64 }
65
66 if (!this->write_command(SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION, this->enable_asc_ ? 1 : 0)) {
67 this->mark_failed(LOG_STR("Error setting automatic self calibration"));
68 return;
69 }
70
71 this->initialized_ = true;
72 // Finally start sensor measurements
73 this->start_measurement_();
74 });
75 });
76}
77
79 static const char *const MM_PERIODIC_STR = "Periodic (5s)";
80 static const char *const MM_LOW_POWER_PERIODIC_STR = "Low power periodic (30s)";
81 static const char *const MM_SINGLE_SHOT_STR = "Single shot";
82 static const char *const MM_SINGLE_SHOT_RHT_ONLY_STR = "Single shot rht only";
83 const char *measurement_mode_str = MM_PERIODIC_STR;
84
85 switch (this->measurement_mode_) {
86 case PERIODIC:
87 // measurement_mode_str = MM_PERIODIC_STR;
88 break;
90 measurement_mode_str = MM_LOW_POWER_PERIODIC_STR;
91 break;
92 case SINGLE_SHOT:
93 measurement_mode_str = MM_SINGLE_SHOT_STR;
94 break;
96 measurement_mode_str = MM_SINGLE_SHOT_RHT_ONLY_STR;
97 break;
98 }
99
100 ESP_LOGCONFIG(TAG, "SCD4X:");
101 LOG_I2C_DEVICE(this);
102 ESP_LOGCONFIG(TAG,
103 " Automatic self calibration: %s\n"
104 " Measurement mode: %s\n"
105 " Temperature offset: %.2f °C",
106 ONOFF(this->enable_asc_), measurement_mode_str, this->temperature_offset_);
107 if (this->ambient_pressure_source_ != nullptr) {
108 ESP_LOGCONFIG(TAG, " Dynamic ambient pressure compensation using '%s'",
109 LOG_STR_ARG(this->ambient_pressure_source_->get_log_name()));
110 } else {
111 if (this->ambient_pressure_) {
112 ESP_LOGCONFIG(TAG,
113 " Altitude compensation disabled\n"
114 " Ambient pressure compensation: %dmBar",
115 this->ambient_pressure_);
116 } else {
117 ESP_LOGCONFIG(TAG,
118 " Ambient pressure compensation disabled\n"
119 " Altitude compensation: %dm",
121 }
122 }
123 LOG_UPDATE_INTERVAL(this);
124 LOG_SENSOR(" ", "CO2", this->co2_sensor_);
125 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
126 LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
127}
128
130 if (!this->initialized_) {
131 return;
132 }
133
134 if (this->ambient_pressure_source_ != nullptr) {
136 if (!std::isnan(pressure)) {
137 this->set_ambient_pressure_compensation(pressure);
138 }
139 }
140
141 uint32_t wait_time = 0;
143 this->start_measurement_();
144 wait_time =
145 this->measurement_mode_ == SINGLE_SHOT ? 5000 : 50; // Single shot measurement takes 5 secs rht mode 50 ms
146 }
147 this->set_timeout(wait_time, [this]() {
148 // Check if data is ready
149 if (!this->write_command(SCD4X_CMD_GET_DATA_READY_STATUS)) {
150 this->status_set_warning();
151 return;
152 }
153
154 uint16_t raw_read_status;
155
156 if (!this->read_data(raw_read_status) || raw_read_status == 0x00) {
157 this->status_set_warning();
158 ESP_LOGW(TAG, "Data not ready");
159 return;
160 }
161
162 if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) {
163 ESP_LOGW(TAG, "Error reading measurement");
164 this->status_set_warning();
165 return; // NO RETRY
166 }
167 // Read off sensor data
168 uint16_t raw_data[3];
169 if (!this->read_data(raw_data, 3)) {
170 this->status_set_warning();
171 return;
172 }
173 if (this->co2_sensor_ != nullptr)
174 this->co2_sensor_->publish_state(raw_data[0]);
175
176 if (this->temperature_sensor_ != nullptr) {
177 const float temperature = -45.0f + (175.0f * (raw_data[1])) / (1 << 16);
178 this->temperature_sensor_->publish_state(temperature);
179 }
180 if (this->humidity_sensor_ != nullptr) {
181 const float humidity = (100.0f * raw_data[2]) / (1 << 16);
182 this->humidity_sensor_->publish_state(humidity);
183 }
184 this->status_clear_warning();
185 }); // set_timeout
186}
187
188bool SCD4XComponent::perform_forced_calibration(uint16_t current_co2_concentration) {
189 /*
190 Operate the SCD4x in the operation mode later used in normal sensor operation (periodic measurement, low power
191 periodic measurement or single shot) for > 3 minutes in an environment with homogeneous and constant CO2
192 concentration before performing a forced recalibration.
193 */
194 if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
195 ESP_LOGE(TAG, "Failed to stop measurements");
196 this->status_set_warning();
197 }
198 this->set_timeout(500, [this, current_co2_concentration]() {
199 if (this->write_command(SCD4X_CMD_PERFORM_FORCED_CALIBRATION, current_co2_concentration)) {
200 ESP_LOGD(TAG, "Setting forced calibration Co2 level %d ppm", current_co2_concentration);
201 // frc takes 400 ms
202 // because this method will be used very rarly
203 // the simple approach with delay is ok
204 delay(400); // NOLINT
205 if (!this->start_measurement_()) {
206 return false;
207 } else {
208 ESP_LOGD(TAG, "Forced calibration complete");
209 }
210 return true;
211 } else {
212 ESP_LOGE(TAG, "Force calibration failed");
213 this->status_set_warning();
214 return false;
215 }
216 });
217 return true;
218}
219
221 if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
222 ESP_LOGE(TAG, "Failed to stop measurements");
223 this->status_set_warning();
224 return false;
225 }
226
227 this->set_timeout(500, [this]() {
228 if (!this->write_command(SCD4X_CMD_FACTORY_RESET)) {
229 ESP_LOGE(TAG, "Failed to send factory reset command");
230 this->status_set_warning();
231 return false;
232 }
233 ESP_LOGD(TAG, "Factory reset complete");
234 return true;
235 });
236 return true;
237}
238
240 uint16_t new_ambient_pressure = static_cast<uint16_t>(pressure_in_hpa);
241 if (!this->initialized_) {
242 this->ambient_pressure_ = new_ambient_pressure;
243 return;
244 }
245 // Only send pressure value if it has changed since last update
246 if (new_ambient_pressure != this->ambient_pressure_) {
247 this->update_ambient_pressure_compensation_(new_ambient_pressure);
248 this->ambient_pressure_ = new_ambient_pressure;
249 } else {
250 ESP_LOGD(TAG, "Ambient pressure compensation skipped; no change required");
251 }
252}
253
255 if (this->write_command(SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION, pressure_in_hpa)) {
256 ESP_LOGD(TAG, "Setting ambient pressure compensation to %d hPa", pressure_in_hpa);
257 return true;
258 } else {
259 ESP_LOGE(TAG, "Error setting ambient pressure compensation");
260 return false;
261 }
262}
263
265 uint16_t measurement_command = SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS;
266 switch (this->measurement_mode_) {
267 case PERIODIC:
268 measurement_command = SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS;
269 break;
271 measurement_command = SCD4X_CMD_START_LOW_POWER_CONTINUOUS_MEASUREMENTS;
272 break;
273 case SINGLE_SHOT:
274 measurement_command = SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT;
275 break;
277 measurement_command = SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT_RHT_ONLY;
278 break;
279 }
280
281 uint8_t remaining_retries = 3;
282 while (remaining_retries) {
283 if (!this->write_command(measurement_command)) {
284 ESP_LOGE(TAG, "Error starting measurements");
285 this->status_set_warning();
286 if (--remaining_retries == 0)
287 return false;
288 delay(50); // NOLINT wait 50 ms and try again
289 continue;
290 }
291 this->status_clear_warning();
292 return true;
293 }
294 return false;
295}
296
297} // namespace esphome::scd4x
void mark_failed()
Mark this component as failed.
void status_clear_error()
Definition component.h:302
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
Definition component.cpp:96
void status_clear_warning()
Definition component.h:296
const LogString * get_log_name() const
Get the name for a "%s" log argument (LOG_STR_ARG).
sensor::Sensor * humidity_sensor_
Definition scd4x.h:42
bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa)
Definition scd4x.cpp:254
sensor::Sensor * ambient_pressure_source_
Definition scd4x.h:43
bool perform_forced_calibration(uint16_t current_co2_concentration)
Definition scd4x.cpp:188
sensor::Sensor * temperature_sensor_
Definition scd4x.h:41
MeasurementMode measurement_mode_
Definition scd4x.h:49
sensor::Sensor * co2_sensor_
Definition scd4x.h:40
void set_ambient_pressure_compensation(float pressure_in_hpa)
Definition scd4x.cpp:239
void dump_config() override
Definition scd4x.cpp:78
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:65
float state
This member variable stores the last state that has passed through all filters.
Definition sensor.h:140
ESPHOME_LOG_TAG(TAG, "scd4x")
@ SINGLE_SHOT_RHT_ONLY
Definition scd4x.h:14
@ LOW_POWER_PERIODIC
Definition scd4x.h:12
void HOT delay(uint32_t ms)
Definition hal.cpp:85
static void uint32_t
uint16_t temperature
Definition sun_gtil2.cpp:12
uint8_t pressure
Definition tt21100.cpp:7