ESPHome 2026.3.0
Loading...
Searching...
No Matches
sen6x.cpp
Go to the documentation of this file.
1#include "sen6x.h"
2#include "esphome/core/hal.h"
3#include "esphome/core/log.h"
4#include <cmath>
5
6namespace esphome::sen6x {
7
8static const char *const TAG = "sen6x";
9
10static constexpr uint8_t POLL_RETRIES = 24; // 24 attempts
11static constexpr uint32_t I2C_READ_DELAY = 20; // 20 ms to wait for I2C read to complete
12static constexpr uint32_t POLL_INTERVAL = 50; // 50 ms between poll attempts
13// Single numeric timeout ID — the chain is sequential so only one is active at a time.
14static constexpr uint32_t TIMEOUT_POLL = 1;
15static constexpr uint16_t SEN6X_CMD_GET_DATA_READY_STATUS = 0x0202;
16static constexpr uint16_t SEN6X_CMD_GET_FIRMWARE_VERSION = 0xD100;
17static constexpr uint16_t SEN6X_CMD_GET_PRODUCT_NAME = 0xD014;
18static constexpr uint16_t SEN6X_CMD_GET_SERIAL_NUMBER = 0xD033;
19
20static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT = 0x0300; // SEN66 only!
21static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN62 = 0x04A3;
22static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN63C = 0x0471;
23static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN65 = 0x0446;
24static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN68 = 0x0467;
25static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN69C = 0x04B5;
26
27static constexpr uint16_t SEN6X_CMD_START_MEASUREMENTS = 0x0021;
28static constexpr uint16_t SEN6X_CMD_RESET = 0xD304;
29
30static inline void set_read_command_and_words(SEN6XComponent::Sen6xType type, uint16_t &read_cmd, uint8_t &read_words) {
31 read_cmd = SEN6X_CMD_READ_MEASUREMENT;
32 read_words = 9;
33 switch (type) {
34 case SEN6XComponent::SEN62:
35 read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN62;
36 read_words = 6;
37 break;
38 case SEN6XComponent::SEN63C:
39 read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN63C;
40 read_words = 7;
41 break;
42 case SEN6XComponent::SEN65:
43 read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN65;
44 read_words = 8;
45 break;
46 case SEN6XComponent::SEN66:
47 read_cmd = SEN6X_CMD_READ_MEASUREMENT;
48 read_words = 9;
49 break;
50 case SEN6XComponent::SEN68:
51 read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN68;
52 read_words = 9;
53 break;
54 case SEN6XComponent::SEN69C:
55 read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN69C;
56 read_words = 10;
57 break;
58 default:
59 break;
60 }
61}
62
64 ESP_LOGCONFIG(TAG, "Setting up sen6x...");
65
66 // the sensor needs 100 ms to enter the idle state
67 this->set_timeout(100, [this]() {
68 // Reset the sensor to ensure a clean state regardless of prior commands or power issues
69 if (!this->write_command(SEN6X_CMD_RESET)) {
70 ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
71 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
72 return;
73 }
74
75 // After reset the sensor needs 100 ms to become ready
76 this->set_timeout(100, [this]() {
77 // Step 1: Read serial number (~25ms with I2C delay)
78 uint16_t raw_serial_number[16];
79 if (!this->get_register(SEN6X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 16, 20)) {
80 ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
81 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
82 return;
83 }
85 ESP_LOGI(TAG, "Serial number: %s", this->serial_number_.c_str());
86
87 // Step 2: Read product name in next loop iteration
88 this->set_timeout(0, [this]() {
89 uint16_t raw_product_name[16];
90 if (!this->get_register(SEN6X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) {
91 ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
92 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
93 return;
94 }
95
97
98 Sen6xType inferred_type = this->infer_type_from_product_name_(this->product_name_);
99 if (this->sen6x_type_ == UNKNOWN) {
100 this->sen6x_type_ = inferred_type;
101 if (inferred_type == UNKNOWN) {
102 ESP_LOGE(TAG, "Unknown product '%s'", this->product_name_.c_str());
103 this->mark_failed();
104 return;
105 }
106 ESP_LOGD(TAG, "Type inferred from product: %s", this->product_name_.c_str());
107 } else if (this->sen6x_type_ != inferred_type && inferred_type != UNKNOWN) {
108 ESP_LOGW(TAG, "Configured type (used) mismatches product '%s'", this->product_name_.c_str());
109 }
110 ESP_LOGI(TAG, "Product: %s", this->product_name_.c_str());
111
112 // Validate configured sensors against detected type and disable unsupported ones
113 const bool has_voc_nox = (this->sen6x_type_ == SEN65 || this->sen6x_type_ == SEN66 ||
114 this->sen6x_type_ == SEN68 || this->sen6x_type_ == SEN69C);
115 const bool has_co2 = (this->sen6x_type_ == SEN63C || this->sen6x_type_ == SEN66 || this->sen6x_type_ == SEN69C);
116 const bool has_hcho = (this->sen6x_type_ == SEN68 || this->sen6x_type_ == SEN69C);
117 if (this->voc_sensor_ && !has_voc_nox) {
118 ESP_LOGE(TAG, "VOC requires SEN65, SEN66, SEN68, or SEN69C");
119 this->voc_sensor_ = nullptr;
120 }
121 if (this->nox_sensor_ && !has_voc_nox) {
122 ESP_LOGE(TAG, "NOx requires SEN65, SEN66, SEN68, or SEN69C");
123 this->nox_sensor_ = nullptr;
124 }
125 if (this->co2_sensor_ && !has_co2) {
126 ESP_LOGE(TAG, "CO2 requires SEN63C, SEN66, or SEN69C");
127 this->co2_sensor_ = nullptr;
128 }
129 if (this->hcho_sensor_ && !has_hcho) {
130 ESP_LOGE(TAG, "Formaldehyde requires SEN68 or SEN69C");
131 this->hcho_sensor_ = nullptr;
132 }
133
134 // Step 3: Read firmware version and start measurements in next loop iteration
135 this->set_timeout(0, [this]() {
136 uint16_t raw_firmware_version = 0;
137 if (!this->get_register(SEN6X_CMD_GET_FIRMWARE_VERSION, raw_firmware_version, 20)) {
138 ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
139 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
140 return;
141 }
142 this->firmware_version_major_ = (raw_firmware_version >> 8) & 0xFF;
143 this->firmware_version_minor_ = raw_firmware_version & 0xFF;
144 ESP_LOGI(TAG, "Firmware: %u.%u", this->firmware_version_major_, this->firmware_version_minor_);
145
146 if (!this->write_command(SEN6X_CMD_START_MEASUREMENTS)) {
147 ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
148 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
149 return;
150 }
151
152 this->set_timeout(60000, [this]() { this->startup_complete_ = true; });
153 this->initialized_ = true;
154 ESP_LOGD(TAG, "Initialized");
155 });
156 });
157 });
158 });
159}
160
161void SEN6XComponent::dump_config() {
162 ESP_LOGCONFIG(TAG,
163 "sen6x:\n"
164 " Product: %s\n"
165 " Serial: %s\n"
166 " Firmware: %u.%u\n"
167 " Address: 0x%02X",
168 this->product_name_.c_str(), this->serial_number_.c_str(), this->firmware_version_major_,
169 this->firmware_version_minor_, this->address_);
170 LOG_UPDATE_INTERVAL(this);
171 LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_);
172 LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_);
173 LOG_SENSOR(" ", "PM 4.0", this->pm_4_0_sensor_);
174 LOG_SENSOR(" ", "PM 10.0", this->pm_10_0_sensor_);
175 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
176 LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
177 LOG_SENSOR(" ", "VOC", this->voc_sensor_);
178 LOG_SENSOR(" ", "NOx", this->nox_sensor_);
179 LOG_SENSOR(" ", "HCHO", this->hcho_sensor_);
180 LOG_SENSOR(" ", "CO2", this->co2_sensor_);
181}
182
183void SEN6XComponent::update() {
184 if (!this->initialized_) {
185 return;
186 }
187
188 // Cancel any in-flight polling from a previous update() cycle.
189 this->cancel_timeout(TIMEOUT_POLL);
190
191 set_read_command_and_words(this->sen6x_type_, this->read_cmd_, this->read_words_);
192
193 // Polling uses chained timeouts to guarantee each I2C operation completes
194 // before the next begins. The flow is:
195 //
196 // poll_data_ready_()
197 // -> write_command (data ready status)
198 // -> timeout I2C_READ_DELAY
199 // -> read_data (check ready flag)
200 // -> if not ready: timeout POLL_INTERVAL -> poll_data_ready_() (retry)
201 // -> if ready: read_measurements_()
202 // -> write_command (read measurement)
203 // -> timeout I2C_READ_DELAY
204 // -> parse_and_publish_measurements_()
205 //
206 // All timeouts share a single ID (TIMEOUT_POLL) since only one is active
207 // at a time. cancel_timeout in update() stops any in-flight chain.
208 this->poll_retries_remaining_ = POLL_RETRIES;
209 this->poll_data_ready_();
210}
211
213 if (this->poll_retries_remaining_ == 0) {
214 this->status_set_warning();
215 ESP_LOGD(TAG, "Data not ready");
216 return;
217 }
218 ESP_LOGV(TAG, "Data ready polling attempt %u",
219 static_cast<unsigned>(POLL_RETRIES - this->poll_retries_remaining_ + 1));
221
222 if (!this->write_command(SEN6X_CMD_GET_DATA_READY_STATUS)) {
223 this->status_set_warning();
224 ESP_LOGD(TAG, "write data ready status error (%d)", this->last_error_);
225 return;
226 }
227
228 this->set_timeout(TIMEOUT_POLL, I2C_READ_DELAY, [this]() {
229 uint16_t raw_read_status;
230 if (!this->read_data(&raw_read_status, 1)) {
231 this->status_set_warning();
232 ESP_LOGD(TAG, "read data ready status error (%d)", this->last_error_);
233 return;
234 }
235
236 if ((raw_read_status & 0x0001) == 0) {
237 // Not ready yet; schedule next attempt after POLL_INTERVAL.
238 this->set_timeout(TIMEOUT_POLL, POLL_INTERVAL, [this]() { this->poll_data_ready_(); });
239 return;
240 }
241
242 this->read_measurements_();
243 });
244}
245
247 if (!this->write_command(this->read_cmd_)) {
248 this->status_set_warning();
249 ESP_LOGD(TAG, "Read measurement failed (%d)", this->last_error_);
250 return;
251 }
252
253 this->set_timeout(TIMEOUT_POLL, I2C_READ_DELAY, [this]() { this->parse_and_publish_measurements_(); });
254}
255
257 uint16_t measurements[10];
258
259 if (!this->read_data(measurements, this->read_words_)) {
260 this->status_set_warning();
261 ESP_LOGD(TAG, "Read data failed (%d)", this->last_error_);
262 return;
263 }
264 int8_t voc_index = -1;
265 int8_t nox_index = -1;
266 int8_t hcho_index = -1;
267 int8_t co2_index = -1;
268 bool co2_uint16 = false;
269 switch (this->sen6x_type_) {
270 case SEN62:
271 break;
272 case SEN63C:
273 co2_index = 6;
274 break;
275 case SEN65:
276 voc_index = 6;
277 nox_index = 7;
278 break;
279 case SEN66:
280 voc_index = 6;
281 nox_index = 7;
282 co2_index = 8;
283 co2_uint16 = true;
284 break;
285 case SEN68:
286 voc_index = 6;
287 nox_index = 7;
288 hcho_index = 8;
289 break;
290 case SEN69C:
291 voc_index = 6;
292 nox_index = 7;
293 hcho_index = 8;
294 co2_index = 9;
295 break;
296 default:
297 break;
298 }
299
300 float pm_1_0 = measurements[0] / 10.0f;
301 if (measurements[0] == 0xFFFF)
302 pm_1_0 = NAN;
303 float pm_2_5 = measurements[1] / 10.0f;
304 if (measurements[1] == 0xFFFF)
305 pm_2_5 = NAN;
306 float pm_4_0 = measurements[2] / 10.0f;
307 if (measurements[2] == 0xFFFF)
308 pm_4_0 = NAN;
309 float pm_10_0 = measurements[3] / 10.0f;
310 if (measurements[3] == 0xFFFF)
311 pm_10_0 = NAN;
312 float humidity = static_cast<int16_t>(measurements[4]) / 100.0f;
313 if (measurements[4] == 0x7FFF)
314 humidity = NAN;
315 float temperature = static_cast<int16_t>(measurements[5]) / 200.0f;
316 if (measurements[5] == 0x7FFF)
317 temperature = NAN;
318
319 float voc = NAN;
320 float nox = NAN;
321 float hcho = NAN;
322 float co2 = NAN;
323
324 if (voc_index >= 0) {
325 voc = static_cast<int16_t>(measurements[voc_index]) / 10.0f;
326 if (measurements[voc_index] == 0x7FFF)
327 voc = NAN;
328 }
329 if (nox_index >= 0) {
330 nox = static_cast<int16_t>(measurements[nox_index]) / 10.0f;
331 if (measurements[nox_index] == 0x7FFF)
332 nox = NAN;
333 }
334
335 if (hcho_index >= 0) {
336 const uint16_t hcho_raw = measurements[hcho_index];
337 hcho = hcho_raw / 10.0f;
338 if (hcho_raw == 0xFFFF)
339 hcho = NAN;
340 }
341
342 if (co2_index >= 0) {
343 if (co2_uint16) {
344 const uint16_t co2_raw = measurements[co2_index];
345 co2 = static_cast<float>(co2_raw);
346 if (co2_raw == 0xFFFF)
347 co2 = NAN;
348 } else {
349 const int16_t co2_raw = static_cast<int16_t>(measurements[co2_index]);
350 co2 = static_cast<float>(co2_raw);
351 if (co2_raw == 0x7FFF)
352 co2 = NAN;
353 }
354 }
355
356 if (!this->startup_complete_) {
357 ESP_LOGD(TAG, "Startup delay, ignoring values");
358 this->status_clear_warning();
359 return;
360 }
361
362 if (this->pm_1_0_sensor_ != nullptr)
363 this->pm_1_0_sensor_->publish_state(pm_1_0);
364 if (this->pm_2_5_sensor_ != nullptr)
365 this->pm_2_5_sensor_->publish_state(pm_2_5);
366 if (this->pm_4_0_sensor_ != nullptr)
367 this->pm_4_0_sensor_->publish_state(pm_4_0);
368 if (this->pm_10_0_sensor_ != nullptr)
369 this->pm_10_0_sensor_->publish_state(pm_10_0);
370 if (this->temperature_sensor_ != nullptr)
371 this->temperature_sensor_->publish_state(temperature);
372 if (this->humidity_sensor_ != nullptr)
373 this->humidity_sensor_->publish_state(humidity);
374 if (this->voc_sensor_ != nullptr)
375 this->voc_sensor_->publish_state(voc);
376 if (this->nox_sensor_ != nullptr)
377 this->nox_sensor_->publish_state(nox);
378 if (this->hcho_sensor_ != nullptr)
379 this->hcho_sensor_->publish_state(hcho);
380 if (this->co2_sensor_ != nullptr)
381 this->co2_sensor_->publish_state(co2);
382
383 this->status_clear_warning();
384}
385
386SEN6XComponent::Sen6xType SEN6XComponent::infer_type_from_product_name_(const std::string &product_name) {
387 if (product_name == "SEN62")
388 return SEN62;
389 if (product_name == "SEN63C")
390 return SEN63C;
391 if (product_name == "SEN65")
392 return SEN65;
393 if (product_name == "SEN66")
394 return SEN66;
395 if (product_name == "SEN68")
396 return SEN68;
397 if (product_name == "SEN69C")
398 return SEN69C;
399 return UNKNOWN;
400}
401
402} // namespace esphome::sen6x
void mark_failed()
Mark this component as failed.
virtual void setup()
Where the component's initialization should happen.
Definition component.cpp:94
void status_set_warning(const char *message=nullptr)
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:451
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_timeout(const std boo cancel_timeout)(const char *name)
Cancel a timeout function.
Definition component.h:473
void status_clear_warning()
Definition component.h:254
Sen6xType infer_type_from_product_name_(const std::string &product_name)
Definition sen6x.cpp:386
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.
static const char * sensirion_convert_to_string_in_place(uint16_t *array, size_t length)
This function performs an in-place conversion of the provided buffer from uint16_t values to big endi...
uint16_t type
static void uint32_t
uint16_t temperature
Definition sun_gtil2.cpp:12