ESPHome 2026.5.0b1
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
7static const char *const 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 ESP_LOGE(TAG, "Failed to stop measurements");
34 this->mark_failed();
35 return;
36 }
37 // According to the SCD4x datasheet the sensor will only respond to other commands after waiting 500 ms after
38 // issuing the stop_periodic_measurement command
39 this->set_timeout(500, [this]() {
40 uint16_t raw_serial_number[3];
41 if (!this->get_register(SCD4X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 1)) {
42 ESP_LOGE(TAG, "Failed to read serial number");
44 this->mark_failed();
45 return;
46 }
47 ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", (uint16_t(raw_serial_number[0]) >> 8),
48 uint16_t(raw_serial_number[0] & 0xFF), (uint16_t(raw_serial_number[1]) >> 8));
49
50 if (!this->write_command(SCD4X_CMD_TEMPERATURE_OFFSET,
51 (uint16_t) (temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) {
52 ESP_LOGE(TAG, "Error setting temperature offset");
54 this->mark_failed();
55 return;
56 }
57
58 // If pressure compensation available use it, else use altitude
59 if (this->ambient_pressure_) {
61 ESP_LOGE(TAG, "Error setting ambient pressure compensation");
63 this->mark_failed();
64 return;
65 }
66 } else {
67 if (!this->write_command(SCD4X_CMD_ALTITUDE_COMPENSATION, this->altitude_compensation_)) {
68 ESP_LOGE(TAG, "Error setting altitude compensation");
70 this->mark_failed();
71 return;
72 }
73 }
74
75 if (!this->write_command(SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION, this->enable_asc_ ? 1 : 0)) {
76 ESP_LOGE(TAG, "Error setting automatic self calibration");
78 this->mark_failed();
79 return;
80 }
81
82 this->initialized_ = true;
83 // Finally start sensor measurements
84 this->start_measurement_();
85 });
86 });
87}
88
90 static const char *const MM_PERIODIC_STR = "Periodic (5s)";
91 static const char *const MM_LOW_POWER_PERIODIC_STR = "Low power periodic (30s)";
92 static const char *const MM_SINGLE_SHOT_STR = "Single shot";
93 static const char *const MM_SINGLE_SHOT_RHT_ONLY_STR = "Single shot rht only";
94 const char *measurement_mode_str = MM_PERIODIC_STR;
95
96 switch (this->measurement_mode_) {
97 case PERIODIC:
98 // measurement_mode_str = MM_PERIODIC_STR;
99 break;
101 measurement_mode_str = MM_LOW_POWER_PERIODIC_STR;
102 break;
103 case SINGLE_SHOT:
104 measurement_mode_str = MM_SINGLE_SHOT_STR;
105 break;
107 measurement_mode_str = MM_SINGLE_SHOT_RHT_ONLY_STR;
108 break;
109 }
110
111 ESP_LOGCONFIG(TAG, "SCD4X:");
112 LOG_I2C_DEVICE(this);
113 if (this->is_failed()) {
114 switch (this->error_code_) {
116 ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
117 break;
119 ESP_LOGW(TAG, "Measurement Initialization failed");
120 break;
122 ESP_LOGW(TAG, "Unable to read firmware version");
123 break;
124 default:
125 ESP_LOGW(TAG, "Unknown setup error");
126 break;
127 }
128 }
129 ESP_LOGCONFIG(TAG,
130 " Automatic self calibration: %s\n"
131 " Measurement mode: %s\n"
132 " Temperature offset: %.2f °C",
133 ONOFF(this->enable_asc_), measurement_mode_str, this->temperature_offset_);
134 if (this->ambient_pressure_source_ != nullptr) {
135 ESP_LOGCONFIG(TAG, " Dynamic ambient pressure compensation using '%s'",
137 } else {
138 if (this->ambient_pressure_) {
139 ESP_LOGCONFIG(TAG,
140 " Altitude compensation disabled\n"
141 " Ambient pressure compensation: %dmBar",
142 this->ambient_pressure_);
143 } else {
144 ESP_LOGCONFIG(TAG,
145 " Ambient pressure compensation disabled\n"
146 " Altitude compensation: %dm",
148 }
149 }
150 LOG_UPDATE_INTERVAL(this);
151 LOG_SENSOR(" ", "CO2", this->co2_sensor_);
152 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
153 LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
154}
155
157 if (!this->initialized_) {
158 return;
159 }
160
161 if (this->ambient_pressure_source_ != nullptr) {
163 if (!std::isnan(pressure)) {
164 this->set_ambient_pressure_compensation(pressure);
165 }
166 }
167
168 uint32_t wait_time = 0;
170 this->start_measurement_();
171 wait_time =
172 this->measurement_mode_ == SINGLE_SHOT ? 5000 : 50; // Single shot measurement takes 5 secs rht mode 50 ms
173 }
174 this->set_timeout(wait_time, [this]() {
175 // Check if data is ready
176 if (!this->write_command(SCD4X_CMD_GET_DATA_READY_STATUS)) {
177 this->status_set_warning();
178 return;
179 }
180
181 uint16_t raw_read_status;
182
183 if (!this->read_data(raw_read_status) || raw_read_status == 0x00) {
184 this->status_set_warning();
185 ESP_LOGW(TAG, "Data not ready");
186 return;
187 }
188
189 if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) {
190 ESP_LOGW(TAG, "Error reading measurement");
191 this->status_set_warning();
192 return; // NO RETRY
193 }
194 // Read off sensor data
195 uint16_t raw_data[3];
196 if (!this->read_data(raw_data, 3)) {
197 this->status_set_warning();
198 return;
199 }
200 if (this->co2_sensor_ != nullptr)
201 this->co2_sensor_->publish_state(raw_data[0]);
202
203 if (this->temperature_sensor_ != nullptr) {
204 const float temperature = -45.0f + (175.0f * (raw_data[1])) / (1 << 16);
205 this->temperature_sensor_->publish_state(temperature);
206 }
207 if (this->humidity_sensor_ != nullptr) {
208 const float humidity = (100.0f * raw_data[2]) / (1 << 16);
209 this->humidity_sensor_->publish_state(humidity);
210 }
211 this->status_clear_warning();
212 }); // set_timeout
213}
214
215bool SCD4XComponent::perform_forced_calibration(uint16_t current_co2_concentration) {
216 /*
217 Operate the SCD4x in the operation mode later used in normal sensor operation (periodic measurement, low power
218 periodic measurement or single shot) for > 3 minutes in an environment with homogeneous and constant CO2
219 concentration before performing a forced recalibration.
220 */
221 if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
222 ESP_LOGE(TAG, "Failed to stop measurements");
223 this->status_set_warning();
224 }
225 this->set_timeout(500, [this, current_co2_concentration]() {
226 if (this->write_command(SCD4X_CMD_PERFORM_FORCED_CALIBRATION, current_co2_concentration)) {
227 ESP_LOGD(TAG, "Setting forced calibration Co2 level %d ppm", current_co2_concentration);
228 // frc takes 400 ms
229 // because this method will be used very rarly
230 // the simple approach with delay is ok
231 delay(400); // NOLINT
232 if (!this->start_measurement_()) {
233 return false;
234 } else {
235 ESP_LOGD(TAG, "Forced calibration complete");
236 }
237 return true;
238 } else {
239 ESP_LOGE(TAG, "Force calibration failed");
240 this->error_code_ = FRC_FAILED;
241 this->status_set_warning();
242 return false;
243 }
244 });
245 return true;
246}
247
249 if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
250 ESP_LOGE(TAG, "Failed to stop measurements");
251 this->status_set_warning();
252 return false;
253 }
254
255 this->set_timeout(500, [this]() {
256 if (!this->write_command(SCD4X_CMD_FACTORY_RESET)) {
257 ESP_LOGE(TAG, "Failed to send factory reset command");
258 this->status_set_warning();
259 return false;
260 }
261 ESP_LOGD(TAG, "Factory reset complete");
262 return true;
263 });
264 return true;
265}
266
268 uint16_t new_ambient_pressure = static_cast<uint16_t>(pressure_in_hpa);
269 if (!this->initialized_) {
270 this->ambient_pressure_ = new_ambient_pressure;
271 return;
272 }
273 // Only send pressure value if it has changed since last update
274 if (new_ambient_pressure != this->ambient_pressure_) {
275 this->update_ambient_pressure_compensation_(new_ambient_pressure);
276 this->ambient_pressure_ = new_ambient_pressure;
277 } else {
278 ESP_LOGD(TAG, "Ambient pressure compensation skipped; no change required");
279 }
280}
281
283 if (this->write_command(SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION, pressure_in_hpa)) {
284 ESP_LOGD(TAG, "Setting ambient pressure compensation to %d hPa", pressure_in_hpa);
285 return true;
286 } else {
287 ESP_LOGE(TAG, "Error setting ambient pressure compensation");
288 return false;
289 }
290}
291
293 uint16_t measurement_command = SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS;
294 switch (this->measurement_mode_) {
295 case PERIODIC:
296 measurement_command = SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS;
297 break;
299 measurement_command = SCD4X_CMD_START_LOW_POWER_CONTINUOUS_MEASUREMENTS;
300 break;
301 case SINGLE_SHOT:
302 measurement_command = SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT;
303 break;
305 measurement_command = SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT_RHT_ONLY;
306 break;
307 }
308
309 uint8_t remaining_retries = 3;
310 while (remaining_retries) {
311 if (!this->write_command(measurement_command)) {
312 ESP_LOGE(TAG, "Error starting measurements");
314 this->status_set_warning();
315 if (--remaining_retries == 0)
316 return false;
317 delay(50); // NOLINT wait 50 ms and try again
318 continue;
319 }
320 this->status_clear_warning();
321 return true;
322 }
323 return false;
324}
325
326} // namespace esphome::scd4x
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:284
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_timeout(const std voi set_timeout)(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a unique name.
Definition component.h:510
void status_clear_error()
Definition component.h:312
void status_clear_warning()
Definition component.h:306
const StringRef & get_name() const
Definition entity_base.h:71
constexpr const char * c_str() const
Definition string_ref.h:73
sensor::Sensor * humidity_sensor_
Definition scd4x.h:50
bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa)
Definition scd4x.cpp:282
sensor::Sensor * ambient_pressure_source_
Definition scd4x.h:51
bool perform_forced_calibration(uint16_t current_co2_concentration)
Definition scd4x.cpp:215
sensor::Sensor * temperature_sensor_
Definition scd4x.h:49
MeasurementMode measurement_mode_
Definition scd4x.h:58
sensor::Sensor * co2_sensor_
Definition scd4x.h:48
void set_ambient_pressure_compensation(float pressure_in_hpa)
Definition scd4x.cpp:267
void dump_config() override
Definition scd4x.cpp:89
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:68
float state
This member variable stores the last state that has passed through all filters.
Definition sensor.h:138
@ COMMUNICATION_FAILED
Definition scd4x.h:11
@ SERIAL_NUMBER_IDENTIFICATION_FAILED
Definition scd4x.h:12
@ MEASUREMENT_INIT_FAILED
Definition scd4x.h:13
@ SINGLE_SHOT_RHT_ONLY
Definition scd4x.h:22
@ LOW_POWER_PERIODIC
Definition scd4x.h:20
void HOT delay(uint32_t ms)
Definition hal.cpp:82
static void uint32_t
uint16_t temperature
Definition sun_gtil2.cpp:12
uint8_t pressure
Definition tt21100.cpp:7