ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
stcc4.cpp
Go to the documentation of this file.
1#include <cstdint>
2
3#include "stcc4.h"
4#include "esphome/core/hal.h"
5#include "esphome/core/log.h"
6
7namespace esphome::stcc4 {
8
9ESPHOME_LOG_TAG(TAG, "stcc4");
10
11// I2C Commands
12static constexpr uint16_t STCC4_CMD_START_CONTINUOUS_MEASUREMENT = 0x218b;
13static constexpr uint16_t STCC4_CMD_STOP_CONTINUOUS_MEASUREMENT = 0x3f86;
14static constexpr uint16_t STCC4_CMD_MEASURE_SINGLE_SHOT = 0x219d;
15static constexpr uint16_t STCC4_CMD_READ_MEASUREMENT = 0xec05;
16static constexpr uint16_t STCC4_CMD_GET_PRODUCT_ID = 0x365b;
17static constexpr uint16_t STCC4_CMD_SET_RHT_COMPENSATION = 0xe000;
18static constexpr uint16_t STCC4_CMD_SET_PRESSURE_COMPENSATION = 0xe016;
19
20// Exit sleep is an 8-bit command (single byte 0x00), not 16-bit
21static constexpr uint8_t STCC4_CMD_EXIT_SLEEP_MODE = 0x00;
22
23static constexpr uint32_t STCC4_PRODUCT_ID = 0x0901018a;
24
25// Timeout for determining when the device is ready for use, in milliseconds.
26// While waiting for the previous measurement to finish, the device will NACK all I2C requests
27// and it can take up to 1200 ms for the operation to complete according to the datasheet.
28static constexpr uint32_t READY_TIMEOUT_MS = 1200;
29
30// Poll interval for determining when the device is ready for use, in milliseconds.
31static constexpr uint32_t READY_POLL_INTERVAL_MS = 100;
32
33// Convert units the the device's representation according to the datasheet.
34constexpr uint16_t temperature_in_c_to_ticks(float temperature_in_c) {
35 return uint16_t((std::clamp(temperature_in_c, -45.f, 130.f) + 45.f) * 65535.f / 175.f);
36}
37
38constexpr float temperature_in_ticks_to_c(uint16_t temperature_in_ticks) {
39 return temperature_in_ticks * 175.f / 65535.f - 45.f;
40}
41
42constexpr uint16_t humidity_in_percent_to_ticks(float humidity_in_percent) {
43 return uint16_t((std::clamp(humidity_in_percent, 0.f, 100.f) + 6.f) * 65535.f / 125.f);
44}
45
46constexpr float humidity_in_ticks_to_percent(uint16_t humidity_in_ticks) {
47 return humidity_in_ticks * 125.f / 65535.f - 6.f;
48}
49
50constexpr uint16_t pressure_in_hpa_to_pa_2(float pressure_in_hpa) {
51 return uint16_t(std::clamp(pressure_in_hpa, 400.f, 1100.f) * 50.f);
52}
53
54constexpr float pressure_in_pa_2_to_hpa(uint16_t pressure_in_pa_2) { return pressure_in_pa_2 / 50.f; }
55
57 this->stop_poller(); // not ready yet
58
59 // Wait 100 ms after power up before attempting to communicate with the sensor
60 this->set_timeout(100, [this]() {
61 // Send exit sleep mode command (8-bit, NACK expected), wait 5 ms to exit sleep
62 this->write_command(STCC4_CMD_EXIT_SLEEP_MODE);
63 this->set_timeout(5, [this]() { this->poll_until_ready_for_setup_or_timeout_(millis()); });
64 });
65}
66
68 // Stop continuous measurements in case they were previously running
69 // The device may NACK this request if it is not ready to communicate yet
70 if (this->write_command(STCC4_CMD_STOP_CONTINUOUS_MEASUREMENT)) {
71 // Read product ID to verify communication (6 words: 2 for product_id + 4 for serial)
72 // The device may NACK this request if it is not ready to communicate yet
73 uint16_t raw_product_id[6];
74 if (this->get_register(STCC4_CMD_GET_PRODUCT_ID, raw_product_id, 6, 1)) {
75 uint32_t product_id = (uint32_t(raw_product_id[0]) << 16) | raw_product_id[1];
76 uint64_t serial_number = (uint64_t(raw_product_id[2]) << 48) | (uint64_t(raw_product_id[3]) << 32) |
77 (uint64_t(raw_product_id[4]) << 16) | raw_product_id[5];
78 ESP_LOGD(TAG, "Product ID: 0x%08" PRIX32 ", Serial: 0x%016" PRIX64, product_id, serial_number);
79 if (product_id != STCC4_PRODUCT_ID) {
80 ESP_LOGE(TAG, "Unsupported product ID");
81 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
82 return;
83 }
84
85 // Set static ambient pressure compensation if configured
86 if (this->ambient_pressure_in_pa_2_ != 0) {
88 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
89 return;
90 }
91 }
92
93 // Apply the dynamic compensation sources' current values, if configured
94 if (this->temperature_source_ != nullptr && this->humidity_source_ != nullptr) {
96 }
97 if (this->ambient_pressure_source_ != nullptr) {
99 }
100
102 this->start_poller();
103 this->finish_setup_();
104 return;
105 }
106
107 // Start continuous measurement
108 if (!this->write_command(STCC4_CMD_START_CONTINUOUS_MEASUREMENT)) {
109 ESP_LOGE(TAG, "Failed to start continuous measurement");
110 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
111 return;
112 }
113 this->schedule_continuous_update_(false);
114 this->finish_setup_();
115 return;
116 }
117 }
118
119 if (millis() - start_time < READY_TIMEOUT_MS) {
120 ESP_LOGVV(TAG, "Retry sync");
121 this->set_timeout(READY_POLL_INTERVAL_MS,
122 [this, start_time]() { this->poll_until_ready_for_setup_or_timeout_(start_time); });
123 return;
124 }
125
126 ESP_LOGE(TAG, "Failed to stop continuous measurements and read product ID");
127 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
128}
129
131 this->ready_ = true;
132 // Follow the sources only once measuring started, so a failed setup stops writing to the device
133 if (this->temperature_source_ != nullptr && this->humidity_source_ != nullptr) {
136 }
137 if (this->ambient_pressure_source_ != nullptr) {
139 [this](float) { this->update_ambient_pressure_compensation_from_source_(); });
140 }
141}
142
143void STCC4Component::dump_config() {
144 ESP_LOGCONFIG(TAG, "STCC4:");
145 LOG_I2C_DEVICE(this);
146 if (this->is_failed()) {
147 ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
148 }
149 ESP_LOGCONFIG(TAG, " Measurement mode: %s",
150 this->measurement_mode_ == MeasurementMode::CONTINUOUS ? LOG_STR_LITERAL("Continuous (1s)")
151 : LOG_STR_LITERAL("Single shot"));
152 if (this->ambient_pressure_source_ != nullptr) {
153 ESP_LOGCONFIG(TAG, " Dynamic ambient pressure compensation using '%s'",
155 } else if (this->ambient_pressure_in_pa_2_ != 0) {
156 ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %f hPa",
158 }
159 if (this->temperature_source_ != nullptr) {
160 ESP_LOGCONFIG(TAG, " Temperature compensation using '%s'", this->temperature_source_->get_name().c_str());
161 }
162 if (this->humidity_source_ != nullptr) {
163 ESP_LOGCONFIG(TAG, " Humidity compensation using '%s'", this->humidity_source_->get_name().c_str());
164 }
165 LOG_UPDATE_INTERVAL(this);
166 LOG_SENSOR(" ", "CO2", this->co2_sensor_);
167 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
168 LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
169}
170
171void STCC4Component::update() {
173 return;
174
175 // Perform single-shot measurement, wait 500 ms for the measurement to be ready
176 if (!this->write_command(STCC4_CMD_MEASURE_SINGLE_SHOT)) {
177 ESP_LOGW(TAG, "Failed to start single shot measurement");
178 this->status_set_warning();
179 return;
180 }
181 this->set_timeout(500, [this]() {
182 if (this->read_measurement_(0)) {
183 this->status_clear_warning();
184 } else {
185 ESP_LOGW(TAG, "Failed to read measurement data");
186 this->status_set_warning();
187 }
188 });
189}
190
191void STCC4Component::schedule_continuous_update_(bool retry_for_clock_drift) {
192 // In continuous measurement mode, the STCC4 produces a sample every 1000 ms according to its
193 // internal clock. The datasheet recommends retrying 150 ms after a failed read to compensate
194 // for clock drift between the host and the device.
195 this->set_timeout(retry_for_clock_drift ? 150 : 1000, [this, retry_for_clock_drift]() {
196 if (this->read_measurement_(retry_for_clock_drift ? 0 : sensirion_common::SENSIRION_OPTION_READ_MAY_NACK)) {
197 this->status_clear_warning();
198 this->schedule_continuous_update_(false);
199 } else if (!retry_for_clock_drift) {
200 this->schedule_continuous_update_(true);
201 } else {
202 this->status_set_warning();
203 this->schedule_continuous_update_(false);
204 }
205 });
206}
207
208bool STCC4Component::read_measurement_(uint8_t sensirion_options) {
209 // Read measurement data: 4 words (CO2, temperature, humidity, status)
210 uint16_t raw_data[4];
211 if (!this->get_register_(STCC4_CMD_READ_MEASUREMENT, ADDR_16_BIT, raw_data, 4, 1, sensirion_options)) {
212 return false;
213 }
214
215 // CO2 value is in ppm as int16 (ignore negative values during warm-up)
216 const int16_t co2_raw = int16_t(raw_data[0]);
217 if (this->co2_sensor_ != nullptr && co2_raw >= 0) {
218 this->co2_sensor_->publish_state(co2_raw);
219 }
220
221 if (this->temperature_sensor_ != nullptr) {
222 this->temperature_sensor_->publish_state(temperature_in_ticks_to_c(raw_data[1]));
223 }
224
225 if (this->humidity_sensor_ != nullptr) {
226 this->humidity_sensor_->publish_state(humidity_in_ticks_to_percent(raw_data[2]));
227 }
228 return true;
229}
230
232 const float temperature_in_c = this->temperature_source_->state;
233 const float humidity_in_percent = this->humidity_source_->state;
234 if (std::isnan(temperature_in_c) || std::isnan(humidity_in_percent))
235 return;
236
237 const uint16_t temperature_in_ticks = temperature_in_c_to_ticks(temperature_in_c);
238 const uint16_t humidity_in_ticks = humidity_in_percent_to_ticks(humidity_in_percent);
239 if ((this->temperature_in_ticks_ != temperature_in_ticks || this->humidity_in_ticks_ != humidity_in_ticks) &&
240 this->write_rht_compensation_(temperature_in_ticks, humidity_in_ticks)) {
241 this->temperature_in_ticks_ = temperature_in_ticks;
242 this->humidity_in_ticks_ = humidity_in_ticks;
243 }
244}
245
246void STCC4Component::set_ambient_pressure_compensation(float pressure_in_hpa) {
247 this->ambient_pressure_in_pa_2_ = pressure_in_hpa_to_pa_2(pressure_in_hpa);
248}
249
251 const float pressure_in_hpa = this->ambient_pressure_source_->state;
252 if (std::isnan(pressure_in_hpa))
253 return;
254
255 if (pressure_in_hpa < 100.f || pressure_in_hpa > 10000.f) {
256 // Some pressure sensors report values in Pa instead of hPa and there's no way to check at compile time.
257 // Warn if the value seems far outside of the expected range.
260 ESP_LOGW(TAG, "Ambient pressure compensation sensor might have incompatible units: got %f hPa", pressure_in_hpa);
261 }
262 return; // skip this update
263 } else {
265 }
266
267 const uint16_t ambient_pressure_in_pa_2 = pressure_in_hpa_to_pa_2(pressure_in_hpa);
268 if (this->ambient_pressure_in_pa_2_ != ambient_pressure_in_pa_2 &&
269 this->write_ambient_pressure_compensation_(ambient_pressure_in_pa_2)) {
270 this->ambient_pressure_in_pa_2_ = ambient_pressure_in_pa_2;
271 }
272}
273
274bool STCC4Component::write_rht_compensation_(uint16_t temperature_in_ticks, uint16_t humidity_in_ticks) {
275 ESP_LOGVV(TAG, "Set RHT compensation: %f °C, %f %%RH", temperature_in_ticks_to_c(temperature_in_ticks),
276 humidity_in_ticks_to_percent(humidity_in_ticks));
277 const uint16_t data[2] = {temperature_in_ticks, humidity_in_ticks};
278 if (!this->write_command(STCC4_CMD_SET_RHT_COMPENSATION, data, 2)) {
279 ESP_LOGE(TAG, "Failed to set RHT compensation");
280 return false;
281 }
282 return true;
283}
284
286 ESP_LOGVV(TAG, "Set pressure compensation: %f hPa", pressure_in_pa_2_to_hpa(pressure_in_pa_2));
287 if (!this->write_command(STCC4_CMD_SET_PRESSURE_COMPENSATION, pressure_in_pa_2)) {
288 ESP_LOGE(TAG, "Failed to set ambient pressure compensation");
289 return false;
290 }
291 return true;
292}
293
294} // namespace esphome::stcc4
void mark_failed()
Mark this component as failed.
virtual void setup()
Where the component's initialization should happen.
Definition component.cpp:84
bool is_failed() const
Definition component.h:274
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:291
const StringRef & get_name() const
Definition entity_base.h:71
constexpr const char * c_str() const
Definition string_ref.h:78
bool get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, uint8_t len, uint8_t delay, uint8_t sensirion_options=0)
get data words from I2C register.
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.
void add_on_state_callback(F &&callback)
Add a callback that will be called every time a filtered value arrives.
Definition sensor.h:121
float state
This member variable stores the last state that has passed through all filters.
Definition sensor.h:140
sensor::Sensor * temperature_source_
Definition stcc4.h:40
sensor::Sensor * ambient_pressure_source_
Definition stcc4.h:42
bool write_rht_compensation_(uint16_t temperature_in_ticks, uint16_t humidity_in_ticks)
Definition stcc4.cpp:274
uint16_t ambient_pressure_in_pa_2_
Definition stcc4.h:48
void schedule_continuous_update_(bool retry_for_clock_drift)
Definition stcc4.cpp:191
bool write_ambient_pressure_compensation_(uint16_t pressure_in_pa_2)
Definition stcc4.cpp:285
void update_rht_compensation_from_source_()
Definition stcc4.cpp:231
void poll_until_ready_for_setup_or_timeout_(uint32_t start_time)
Definition stcc4.cpp:67
bool read_measurement_(uint8_t sensirion_options)
Definition stcc4.cpp:208
MeasurementMode measurement_mode_
Definition stcc4.h:44
bool ambient_pressure_unit_warning_logged_
Definition stcc4.h:49
sensor::Sensor * humidity_source_
Definition stcc4.h:41
void update_ambient_pressure_compensation_from_source_()
Definition stcc4.cpp:250
constexpr float temperature_in_ticks_to_c(uint16_t temperature_in_ticks)
Definition stcc4.cpp:38
constexpr uint16_t humidity_in_percent_to_ticks(float humidity_in_percent)
Definition stcc4.cpp:42
constexpr uint16_t pressure_in_hpa_to_pa_2(float pressure_in_hpa)
Definition stcc4.cpp:50
constexpr float humidity_in_ticks_to_percent(uint16_t humidity_in_ticks)
Definition stcc4.cpp:46
ESPHOME_LOG_TAG(TAG, "stcc4")
constexpr float pressure_in_pa_2_to_hpa(uint16_t pressure_in_pa_2)
Definition stcc4.cpp:54
constexpr uint16_t temperature_in_c_to_ticks(float temperature_in_c)
Definition stcc4.cpp:34
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
static void uint32_t
char serial_number[10]
Definition sun_gtil2.cpp:15