ESPHome 2026.11.0-dev
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 <cinttypes>
5#include <cmath>
6#include <iterator>
7
8namespace esphome::sen6x {
9
10ESPHOME_LOG_TAG(TAG, "sen6x");
11
12#ifdef SEN6X_STATUS_SENSOR_COUNT
13// Bit of the status register behind each kind; the CO2 error bit is 9 on SEN66 and 12 on SEN63C/SEN69C
14static uint8_t status_bit(Sen6xStatus status, SEN6XComponent::Sen6xType type) {
15 switch (status) {
17 return 4;
19 return 21;
21 return 6;
23 return 7;
25 return type == SEN6XComponent::SEN66 ? 9 : 12;
27 return 10;
29 return 11;
30 }
31 return 0;
32}
33#endif
34
35static constexpr uint8_t POLL_RETRIES = 24; // 24 attempts
36static constexpr uint32_t I2C_READ_DELAY = 20; // 20 ms to wait for I2C read to complete
37static constexpr uint32_t CMD_EXEC_DELAY = 20; // execution time of set commands (datasheet section 4.8)
38static constexpr uint32_t POLL_INTERVAL = 50; // 50 ms between poll attempts
39// Numeric timeout IDs. Each chain is sequential, so only one timeout per ID is active at a time.
40static constexpr uint32_t TIMEOUT_POLL = 1;
41static constexpr uint32_t TIMEOUT_SETUP_STEP = 2;
42static constexpr uint32_t TIMEOUT_STARTUP = 3;
43// Post-command wait times from the datasheet: start execution (4.8.1), stop execution (4.8.2),
44// fan cleaning (4.8.22), SHT heater (4.8.23), CO2 conditioning after start on SEN63C/SEN69C (4.8.1)
45static constexpr uint32_t START_MEASUREMENT_DELAY = 50;
46static constexpr uint32_t STOP_MEASUREMENT_DELAY = 1400;
47static constexpr uint32_t FAN_CLEANING_DELAY = 10000;
48static constexpr uint32_t SHT_HEATER_DELAY = 20000;
49static constexpr uint32_t CO2_CONDITIONING_DELAY = 24000;
50// Accepted range of a runtime pressure source, the same as the schema's ambient_pressure_compensation
51static constexpr float AMBIENT_PRESSURE_MIN_HPA = 700.0f;
52static constexpr float AMBIENT_PRESSURE_MAX_HPA = 1200.0f;
53static constexpr uint16_t SEN6X_CMD_GET_DATA_READY_STATUS = 0x0202;
54static constexpr uint16_t SEN6X_CMD_GET_FIRMWARE_VERSION = 0xD100;
55static constexpr uint16_t SEN6X_CMD_GET_PRODUCT_NAME = 0xD014;
56static constexpr uint16_t SEN6X_CMD_GET_SERIAL_NUMBER = 0xD033;
57static constexpr uint16_t SEN6X_CMD_READ_AND_CLEAR_DEVICE_STATUS = 0xD210;
58
59static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT = 0x0300; // SEN66 only!
60static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN62 = 0x04A3;
61static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN63C = 0x0471;
62static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN65 = 0x0446;
63static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN68 = 0x0467;
64static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN69C = 0x04B5;
65
66#ifdef USE_SEN6X_NUMBER_CONCENTRATION
67static constexpr uint16_t SEN6X_CMD_READ_NUMBER_CONCENTRATION = 0x0316;
68#endif
69static constexpr uint16_t SEN6X_CMD_START_MEASUREMENTS = 0x0021;
70static constexpr uint16_t SEN6X_CMD_STOP_MEASUREMENTS = 0x0104;
71static constexpr uint16_t SEN6X_CMD_START_FAN_CLEANING = 0x5607;
72static constexpr uint16_t SEN6X_CMD_ACTIVATE_SHT_HEATER = 0x6765;
73static constexpr uint16_t SEN6X_CMD_RESET = 0xD304;
74static constexpr uint16_t SEN6X_CMD_VOC_ALGORITHM_TUNING = 0x60D0;
75static constexpr uint16_t SEN6X_CMD_NOX_ALGORITHM_TUNING = 0x60E1;
76static constexpr uint16_t SEN6X_CMD_TEMPERATURE_COMPENSATION = 0x60B2;
77static constexpr uint16_t SEN6X_CMD_TEMPERATURE_ACCELERATION = 0x6100;
78static constexpr uint16_t SEN6X_CMD_CO2_AUTOMATIC_SELF_CAL = 0x6711;
79static constexpr uint16_t SEN6X_CMD_AMBIENT_PRESSURE = 0x6720;
80static constexpr uint16_t SEN6X_CMD_SENSOR_ALTITUDE = 0x6736;
81
82static inline void set_read_command_and_words(SEN6XComponent::Sen6xType type, uint16_t &read_cmd, uint8_t &read_words) {
83 read_cmd = SEN6X_CMD_READ_MEASUREMENT;
84 read_words = 9;
85 switch (type) {
86 case SEN6XComponent::SEN62:
87 read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN62;
88 read_words = 6;
89 break;
90 case SEN6XComponent::SEN63C:
91 read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN63C;
92 read_words = 7;
93 break;
94 case SEN6XComponent::SEN65:
95 read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN65;
96 read_words = 8;
97 break;
98 case SEN6XComponent::SEN66:
99 read_cmd = SEN6X_CMD_READ_MEASUREMENT;
100 read_words = 9;
101 break;
102 case SEN6XComponent::SEN68:
103 read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN68;
104 read_words = 9;
105 break;
106 case SEN6XComponent::SEN69C:
107 read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN69C;
108 read_words = 10;
109 break;
110 default:
111 break;
112 }
113}
114
116 ESP_LOGCONFIG(TAG, "Setting up sen6x...");
117
118 // the sensor needs 100 ms to enter the idle state
119 this->set_timeout(100, [this]() {
120 // Reset the sensor to ensure a clean state regardless of prior commands or power issues
121 if (!this->write_command(SEN6X_CMD_RESET)) {
122 ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
123 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
124 return;
125 }
126
127 // After reset the sensor needs 100 ms to become ready
128 this->set_timeout(100, [this]() {
129 // Step 1: Read serial number (~25ms with I2C delay)
130 uint16_t raw_serial_number[16];
131 if (!this->get_register(SEN6X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 16, 20)) {
132 ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
133 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
134 return;
135 }
137 ESP_LOGI(TAG, "Serial number: %s", this->serial_number_.c_str());
138
139 // Step 2: Read product name in next loop iteration
140 this->set_timeout(0, [this]() {
141 uint16_t raw_product_name[16];
142 if (!this->get_register(SEN6X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) {
143 ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
144 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
145 return;
146 }
147
149
150 Sen6xType inferred_type = this->infer_type_from_product_name_(this->product_name_);
151 if (this->sen6x_type_ == UNKNOWN) {
152 this->sen6x_type_ = inferred_type;
153 if (inferred_type == UNKNOWN) {
154 ESP_LOGE(TAG, "Unknown product '%s'", this->product_name_.c_str());
155 this->mark_failed();
156 return;
157 }
158 ESP_LOGD(TAG, "Type inferred from product: %s", this->product_name_.c_str());
159 } else if (this->sen6x_type_ != inferred_type && inferred_type != UNKNOWN) {
160 ESP_LOGW(TAG, "Configured type (used) mismatches product '%s'", this->product_name_.c_str());
161 }
162 ESP_LOGI(TAG, "Product: %s", this->product_name_.c_str());
163
164 // Validate configured sensors against detected type and disable unsupported ones
165 const bool has_voc_nox = (this->sen6x_type_ == SEN65 || this->sen6x_type_ == SEN66 ||
166 this->sen6x_type_ == SEN68 || this->sen6x_type_ == SEN69C);
167 const bool has_co2 = (this->sen6x_type_ == SEN63C || this->sen6x_type_ == SEN66 || this->sen6x_type_ == SEN69C);
168 const bool has_hcho = (this->sen6x_type_ == SEN68 || this->sen6x_type_ == SEN69C);
169 if (this->voc_sensor_ && !has_voc_nox) {
170 ESP_LOGE(TAG, "VOC requires SEN65, SEN66, SEN68, or SEN69C");
171 this->voc_sensor_ = nullptr;
172 }
173 if (this->nox_sensor_ && !has_voc_nox) {
174 ESP_LOGE(TAG, "NOx requires SEN65, SEN66, SEN68, or SEN69C");
175 this->nox_sensor_ = nullptr;
176 }
177 if (this->co2_sensor_ && !has_co2) {
178 ESP_LOGE(TAG, "CO2 requires SEN63C, SEN66, or SEN69C");
179 this->co2_sensor_ = nullptr;
180 }
181 if (this->hcho_sensor_ && !has_hcho) {
182 ESP_LOGE(TAG, "Formaldehyde requires SEN68 or SEN69C");
183 this->hcho_sensor_ = nullptr;
184 }
185#ifdef SEN6X_STATUS_SENSOR_COUNT
186 // A status sensor for a feature this variant lacks is dropped like the measurement sensors above, so
187 // an empty table skips the status read; the kept ones learn their register bit here
188 size_t kept = 0;
189 for (auto entry : this->status_sensors_) {
190 if ((entry.status == Sen6xStatus::SEN6X_STATUS_GAS_ERROR && !has_voc_nox) ||
191 (entry.status == Sen6xStatus::SEN6X_STATUS_CO2_ERROR && !has_co2) ||
192 (entry.status == Sen6xStatus::SEN6X_STATUS_HCHO_ERROR && !has_hcho)) {
193 ESP_LOGE(TAG, "'%s' needs a variant with that feature", LOG_STR_ARG(entry.sensor->get_log_name()));
194 continue;
195 }
196 entry.bit = status_bit(entry.status, this->sen6x_type_);
197 this->status_sensors_[kept++] = entry;
198 }
199 this->status_sensors_.resize(kept);
200#endif
201
202 // Step 3: Read firmware version and start measurements in next loop iteration
203 this->set_timeout(0, [this]() {
204 uint16_t raw_firmware_version = 0;
205 if (!this->get_register(SEN6X_CMD_GET_FIRMWARE_VERSION, raw_firmware_version, 20)) {
206 ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
207 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
208 return;
209 }
210 this->firmware_version_major_ = (raw_firmware_version >> 8) & 0xFF;
211 this->firmware_version_minor_ = raw_firmware_version & 0xFF;
212 ESP_LOGI(TAG, "Firmware: %u.%u", this->firmware_version_major_, this->firmware_version_minor_);
213
214 // Step 4: write configuration commands one at a time, then start measurements.
215 // Delay the first step so it doesn't run in the same loop tick as the read above.
216 this->set_timeout(TIMEOUT_SETUP_STEP, CMD_EXEC_DELAY, [this]() { this->run_next_setup_step_(); });
217 });
218 });
219 });
220 });
221}
222
223// One configuration write per invocation, spaced by CMD_EXEC_DELAY, then the start command.
224// These writes are optional, so a failure only warns and the chain carries on.
226 while (this->setup_step_index_ < SETUP_STEP_COUNT) {
227 if (this->write_setup_step_(static_cast<SetupStep>(this->setup_step_index_++))) {
228 this->set_timeout(TIMEOUT_SETUP_STEP, CMD_EXEC_DELAY, [this]() { this->run_next_setup_step_(); });
229 return;
230 }
231 }
232 this->finish_setup_();
233}
234
235// Returns false when the step has nothing to write: the setting is not configured, or setup()
236// disabled its sensor for this variant
238 switch (step) {
240 if (this->voc_sensor_ == nullptr || !this->voc_tuning_params_.has_value())
241 return false;
242 this->write_tuning_parameters_(SEN6X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value());
243 return true;
245 if (this->nox_sensor_ == nullptr || !this->nox_tuning_params_.has_value())
246 return false;
247 this->write_tuning_parameters_(SEN6X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value());
248 return true;
250 if (!this->temperature_compensation_.has_value())
251 return false;
253 return true;
255 if (!this->temperature_acceleration_.has_value())
256 return false;
258 return true;
260 if (this->co2_sensor_ == nullptr || !this->co2_asc_.has_value())
261 return false;
262 this->write_setup_register_(SEN6X_CMD_CO2_AUTOMATIC_SELF_CAL, this->co2_asc_.value() ? 1 : 0);
263 return true;
265 if (this->co2_sensor_ == nullptr || this->altitude_compensation_ == ALTITUDE_UNSET)
266 return false;
267 this->write_setup_register_(SEN6X_CMD_SENSOR_ALTITUDE, this->altitude_compensation_);
268 return true;
270 if (this->co2_sensor_ == nullptr || this->ambient_pressure_ == PRESSURE_UNSET)
271 return false;
273 return true;
274 default:
275 return false;
276 }
277}
278
280 // Word 4 selects compensation slot 0; other slots are not exposed
281 uint16_t params[4] = {static_cast<uint16_t>(compensation.offset),
282 static_cast<uint16_t>(compensation.normalized_offset_slope), compensation.time_constant, 0};
283 return this->write_config_words_(SEN6X_CMD_TEMPERATURE_COMPENSATION, params, 4);
284}
285
287 uint16_t params[4] = {acceleration.k, acceleration.p, acceleration.t1, acceleration.t2};
288 return this->write_config_words_(SEN6X_CMD_TEMPERATURE_ACCELERATION, params, 4);
289}
290
291bool SEN6XComponent::write_setup_register_(uint16_t i2c_command, uint16_t value) {
292 return this->write_config_words_(i2c_command, &value, 1);
293}
294
296 if (!this->write_command(SEN6X_CMD_START_MEASUREMENTS)) {
297 ESP_LOGE(TAG, "Write 0x%04X failed, error %d", SEN6X_CMD_START_MEASUREMENTS, this->last_error_);
298 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
299 return;
300 }
301
303 this->initialized_ = true;
304 ESP_LOGD(TAG, "Initialized");
305}
306
307// Writes one optional configuration command and reserves its execution time. A failure warns and
308// returns false, but does not stop setup: the sensor still measures with that setting left at its default.
309bool SEN6XComponent::write_config_words_(uint16_t i2c_command, const uint16_t *data, uint8_t len) {
310 if (!this->write_command(i2c_command, data, len)) {
311 ESP_LOGE(TAG, "Write 0x%04X failed, error %d", i2c_command, this->last_error_);
312 this->status_set_warning();
313 return false;
314 }
315 this->set_command_wait_(CMD_EXEC_DELAY);
316 return true;
317}
318
319bool SEN6XComponent::write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning) {
320 uint16_t params[6] = {tuning.index_offset,
324 tuning.std_initial,
325 tuning.gain_factor};
326 return this->write_config_words_(i2c_command, params, 6);
327}
328
329void SEN6XComponent::dump_config() {
330 ESP_LOGCONFIG(TAG,
331 "sen6x:\n"
332 " Product: %s\n"
333 " Serial: %s\n"
334 " Firmware: %u.%u\n"
335 " Address: 0x%02X\n"
336 " Startup delay: %" PRIu32 " ms",
337 this->product_name_.c_str(), this->serial_number_.c_str(), this->firmware_version_major_,
338 this->firmware_version_minor_, this->address_, this->startup_delay_ms_);
339 LOG_UPDATE_INTERVAL(this);
340 if (this->temperature_compensation_.has_value()) {
341 const auto &comp = this->temperature_compensation_.value();
342 ESP_LOGCONFIG(TAG, " Temperature compensation: offset=%.2f slope=%.4f time_constant=%us", comp.offset / 200.0f,
343 comp.normalized_offset_slope / 10000.0f, comp.time_constant);
344 }
345 if (this->temperature_acceleration_.has_value()) {
346 const auto &accel = this->temperature_acceleration_.value();
347 ESP_LOGCONFIG(TAG, " Temperature acceleration: K=%.1f P=%.1f T1=%.1f T2=%.1f", accel.k / 10.0f, accel.p / 10.0f,
348 accel.t1 / 10.0f, accel.t2 / 10.0f);
349 }
350 // setup() clears co2_sensor_ once it detects a variant without CO2
351 if (this->co2_sensor_ != nullptr) {
352 if (this->co2_asc_.has_value()) {
353 ESP_LOGCONFIG(TAG, " CO2 automatic self-calibration: %s", ONOFF(this->co2_asc_.value()));
354 }
356 ESP_LOGCONFIG(TAG, " Altitude compensation: %u m", this->altitude_compensation_);
357 }
358 if (this->ambient_pressure_ != PRESSURE_UNSET) {
359 ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %u hPa", this->ambient_pressure_);
360 }
361 if (this->ambient_pressure_source_ != nullptr) {
362 ESP_LOGCONFIG(TAG, " Ambient pressure compensation source: %s",
363 LOG_STR_ARG(this->ambient_pressure_source_->get_log_name()));
364 }
365 }
366 LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_);
367 LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_);
368 LOG_SENSOR(" ", "PM 4.0", this->pm_4_0_sensor_);
369 LOG_SENSOR(" ", "PM 10.0", this->pm_10_0_sensor_);
370#ifdef USE_SEN6X_NUMBER_CONCENTRATION
371 LOG_SENSOR(" ", "PMC 0.5", this->pmc_0_5_sensor_);
372 LOG_SENSOR(" ", "PMC 1.0", this->pmc_1_0_sensor_);
373 LOG_SENSOR(" ", "PMC 2.5", this->pmc_2_5_sensor_);
374 LOG_SENSOR(" ", "PMC 4.0", this->pmc_4_0_sensor_);
375 LOG_SENSOR(" ", "PMC 10.0", this->pmc_10_0_sensor_);
376#endif
377 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
378 LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
379 LOG_SENSOR(" ", "VOC", this->voc_sensor_);
380 LOG_SENSOR(" ", "NOx", this->nox_sensor_);
381 LOG_SENSOR(" ", "HCHO", this->hcho_sensor_);
382 LOG_SENSOR(" ", "CO2", this->co2_sensor_);
383#ifdef SEN6X_STATUS_SENSOR_COUNT
384 for (const auto &entry : this->status_sensors_) {
385 LOG_BINARY_SENSOR(" ", "Device status", entry.sensor);
386 }
387#endif
388}
389
390void SEN6XComponent::update() {
391 if (!this->initialized_ || !this->measuring_) {
392 return;
393 }
394 // Cancel any in-flight polling from a previous update() cycle before touching the bus.
395 this->cancel_timeout(TIMEOUT_POLL);
396 // A command is still executing (an action moments ago): run this cycle once its window has passed
397 if (const uint32_t wait = this->command_wait_remaining_(); wait != 0) {
398 this->set_timeout(TIMEOUT_POLL, wait, [this]() { this->update(); });
399 return;
400 }
401
402 if (this->ambient_pressure_source_ != nullptr && this->co2_sensor_ != nullptr) {
404 }
405
406 set_read_command_and_words(this->sen6x_type_, this->read_cmd_, this->read_words_);
407
408 // Polling uses chained timeouts to guarantee each I2C operation completes
409 // before the next begins. The flow is:
410 //
411 // read_device_status_()
412 // with status sensors: write_command (read and clear device status)
413 // -> timeout I2C_READ_DELAY -> parse_and_publish_device_status_()
414 // -> poll_data_ready_()
415 //
416 // poll_data_ready_()
417 // -> write_command (data ready status)
418 // -> timeout I2C_READ_DELAY
419 // -> read_data (check ready flag)
420 // -> if not ready: timeout POLL_INTERVAL -> poll_data_ready_() (retry)
421 // -> if ready: read_measurements_()
422 // -> write_command (read measurement)
423 // -> timeout I2C_READ_DELAY
424 // -> parse_and_publish_measurements_()
425 // -> with pmc_* sensors: read_number_concentration_()
426 // -> write_command (read number concentration)
427 // -> timeout I2C_READ_DELAY
428 // -> parse_and_publish_number_concentration_()
429 //
430 // All timeouts share a single ID (TIMEOUT_POLL) since only one is active
431 // at a time. cancel_timeout in update() stops any in-flight chain.
432 this->poll_retries_remaining_ = POLL_RETRIES;
433 this->read_device_status_();
434}
435
437 // stop_measurement() may have run from a publish callback while the chain was in flight
438 if (!this->measuring_)
439 return;
440 if (this->poll_retries_remaining_ == 0) {
441 this->status_set_warning();
442 ESP_LOGD(TAG, "Data not ready");
443 return;
444 }
445 // A set command (the pressure write above all) gets its execution time before the chain writes again
446 if (const uint32_t wait = this->command_wait_remaining_(); wait != 0) {
447 this->set_timeout(TIMEOUT_POLL, wait, [this]() { this->poll_data_ready_(); });
448 return;
449 }
450 ESP_LOGV(TAG, "Data ready polling attempt %u",
451 static_cast<unsigned>(POLL_RETRIES - this->poll_retries_remaining_ + 1));
453
454 if (!this->write_command(SEN6X_CMD_GET_DATA_READY_STATUS)) {
455 this->status_set_warning();
456 ESP_LOGD(TAG, "write data ready status error (%d)", this->last_error_);
457 return;
458 }
459
460 this->set_timeout(TIMEOUT_POLL, I2C_READ_DELAY, [this]() {
461 uint16_t raw_read_status;
462 if (!this->read_data(&raw_read_status, 1)) {
463 this->status_set_warning();
464 ESP_LOGD(TAG, "read data ready status error (%d)", this->last_error_);
465 return;
466 }
467
468 if ((raw_read_status & 0x0001) == 0) {
469 // Not ready yet; schedule next attempt after POLL_INTERVAL.
470 this->set_timeout(TIMEOUT_POLL, POLL_INTERVAL, [this]() { this->poll_data_ready_(); });
471 return;
472 }
473
474 this->read_measurements_();
475 });
476}
477
479 if (!this->write_command(this->read_cmd_)) {
480 this->status_set_warning();
481 ESP_LOGD(TAG, "Read measurement failed (%d)", this->last_error_);
482 return;
483 }
484
485 this->set_timeout(TIMEOUT_POLL, I2C_READ_DELAY, [this]() { this->parse_and_publish_measurements_(); });
486}
487
489 uint16_t measurements[10];
490
491 if (!this->read_data(measurements, this->read_words_)) {
492 this->status_set_warning();
493 ESP_LOGD(TAG, "Read data failed (%d)", this->last_error_);
494 return;
495 }
496 int8_t voc_index = -1;
497 int8_t nox_index = -1;
498 int8_t hcho_index = -1;
499 int8_t co2_index = -1;
500 bool co2_uint16 = false;
501 switch (this->sen6x_type_) {
502 case SEN62:
503 break;
504 case SEN63C:
505 co2_index = 6;
506 break;
507 case SEN65:
508 voc_index = 6;
509 nox_index = 7;
510 break;
511 case SEN66:
512 voc_index = 6;
513 nox_index = 7;
514 co2_index = 8;
515 co2_uint16 = true;
516 break;
517 case SEN68:
518 voc_index = 6;
519 nox_index = 7;
520 hcho_index = 8;
521 break;
522 case SEN69C:
523 voc_index = 6;
524 nox_index = 7;
525 hcho_index = 8;
526 co2_index = 9;
527 break;
528 default:
529 break;
530 }
531
532 float pm_1_0 = measurements[0] / 10.0f;
533 if (measurements[0] == 0xFFFF)
534 pm_1_0 = NAN;
535 float pm_2_5 = measurements[1] / 10.0f;
536 if (measurements[1] == 0xFFFF)
537 pm_2_5 = NAN;
538 float pm_4_0 = measurements[2] / 10.0f;
539 if (measurements[2] == 0xFFFF)
540 pm_4_0 = NAN;
541 float pm_10_0 = measurements[3] / 10.0f;
542 if (measurements[3] == 0xFFFF)
543 pm_10_0 = NAN;
544 float humidity = static_cast<int16_t>(measurements[4]) / 100.0f;
545 if (measurements[4] == 0x7FFF)
546 humidity = NAN;
547 float temperature = static_cast<int16_t>(measurements[5]) / 200.0f;
548 if (measurements[5] == 0x7FFF)
549 temperature = NAN;
550
551 float voc = NAN;
552 float nox = NAN;
553 float hcho = NAN;
554 float co2 = NAN;
555
556 if (voc_index >= 0) {
557 voc = static_cast<int16_t>(measurements[voc_index]) / 10.0f;
558 if (measurements[voc_index] == 0x7FFF)
559 voc = NAN;
560 }
561 if (nox_index >= 0) {
562 nox = static_cast<int16_t>(measurements[nox_index]) / 10.0f;
563 if (measurements[nox_index] == 0x7FFF)
564 nox = NAN;
565 }
566
567 if (hcho_index >= 0) {
568 const uint16_t hcho_raw = measurements[hcho_index];
569 hcho = hcho_raw / 10.0f;
570 if (hcho_raw == 0xFFFF)
571 hcho = NAN;
572 }
573
574 if (co2_index >= 0) {
575 if (co2_uint16) {
576 const uint16_t co2_raw = measurements[co2_index];
577 co2 = static_cast<float>(co2_raw);
578 if (co2_raw == 0xFFFF)
579 co2 = NAN;
580 } else {
581 const int16_t co2_raw = static_cast<int16_t>(measurements[co2_index]);
582 co2 = static_cast<float>(co2_raw);
583 if (co2_raw == 0x7FFF)
584 co2 = NAN;
585 }
586 }
587
588 if (!this->startup_complete_) {
589 ESP_LOGD(TAG, "Startup delay, ignoring values");
590 this->status_clear_warning();
591 return;
592 }
593
594 if (this->pm_1_0_sensor_ != nullptr)
595 this->pm_1_0_sensor_->publish_state(pm_1_0);
596 if (this->pm_2_5_sensor_ != nullptr)
597 this->pm_2_5_sensor_->publish_state(pm_2_5);
598 if (this->pm_4_0_sensor_ != nullptr)
599 this->pm_4_0_sensor_->publish_state(pm_4_0);
600 if (this->pm_10_0_sensor_ != nullptr)
601 this->pm_10_0_sensor_->publish_state(pm_10_0);
602 if (this->temperature_sensor_ != nullptr)
603 this->temperature_sensor_->publish_state(temperature);
604 if (this->humidity_sensor_ != nullptr)
605 this->humidity_sensor_->publish_state(humidity);
606 if (this->voc_sensor_ != nullptr)
607 this->voc_sensor_->publish_state(voc);
608 if (this->nox_sensor_ != nullptr)
609 this->nox_sensor_->publish_state(nox);
610 if (this->hcho_sensor_ != nullptr)
611 this->hcho_sensor_->publish_state(hcho);
612 if (this->co2_sensor_ != nullptr)
613 this->co2_sensor_->publish_state(co2);
614
615#ifdef USE_SEN6X_NUMBER_CONCENTRATION
616 // The define says some device in the build has pmc sensors; this one may not
618 this->read_number_concentration_(); // the chain ends there, with the warning cleared
619 return;
620 }
621#endif
622 this->status_clear_warning();
623}
624
625// Reads the status register first, so the error sensors update even when the measurement read fails, then runs
626// the measurement poll chain. Read And Clear (0xD210) is used because error flags are sticky on the device;
627// clearing each cycle makes the sensors report whether a problem occurred since the last update.
629#ifdef SEN6X_STATUS_SENSOR_COUNT
630 if (!this->status_sensors_.empty()) {
631 // The pressure write in update() may still be executing; the device rejects commands until it is done
632 if (const uint32_t wait = this->command_wait_remaining_(); wait != 0) {
633 this->set_timeout(TIMEOUT_POLL, wait, [this]() { this->read_device_status_(); });
634 return;
635 }
636 if (this->write_command(SEN6X_CMD_READ_AND_CLEAR_DEVICE_STATUS)) {
637 this->set_timeout(TIMEOUT_POLL, I2C_READ_DELAY, [this]() { this->parse_and_publish_device_status_(); });
638 return;
639 }
640 ESP_LOGD(TAG, "Read device status failed (%d)", this->last_error_);
641 }
642#endif
643 this->poll_data_ready_();
644}
645
646#ifdef SEN6X_STATUS_SENSOR_COUNT
648 uint16_t words[2];
649 if (this->read_data(words, 2)) {
650 const uint32_t status = (static_cast<uint32_t>(words[0]) << 16) | words[1];
651 for (const auto &entry : this->status_sensors_)
652 entry.sensor->publish_state(((status >> entry.bit) & 1) != 0);
653 } else {
654 ESP_LOGD(TAG, "Read device status failed (%d)", this->last_error_);
655 }
656 this->poll_data_ready_();
657}
658#endif
659
660#ifdef USE_SEN6X_NUMBER_CONCENTRATION
662 return this->pmc_0_5_sensor_ != nullptr || this->pmc_1_0_sensor_ != nullptr || this->pmc_2_5_sensor_ != nullptr ||
663 this->pmc_4_0_sensor_ != nullptr || this->pmc_10_0_sensor_ != nullptr;
664}
665
667 if (!this->write_command(SEN6X_CMD_READ_NUMBER_CONCENTRATION)) {
668 this->status_set_warning();
669 ESP_LOGD(TAG, "Read number concentration failed (%d)", this->last_error_);
670 return;
671 }
672
673 this->set_timeout(TIMEOUT_POLL, I2C_READ_DELAY, [this]() { this->parse_and_publish_number_concentration_(); });
674}
675
677 sensor::Sensor *sensors[] = {this->pmc_0_5_sensor_, this->pmc_1_0_sensor_, this->pmc_2_5_sensor_,
678 this->pmc_4_0_sensor_, this->pmc_10_0_sensor_};
679 uint16_t measurements[std::size(sensors)];
680
681 if (!this->status_comm_result(this->read_data(measurements, std::size(measurements)))) {
682 ESP_LOGD(TAG, "Read data failed (%d)", this->last_error_);
683 return;
684 }
685
686 for (size_t i = 0; i < std::size(sensors); i++) {
687 if (sensors[i] != nullptr)
688 sensors[i]->publish_state(measurements[i] == 0xFFFF ? NAN : measurements[i] / 10.0f);
689 }
690}
691#endif
692
693void SEN6XComponent::write_ambient_pressure_(uint16_t pressure_hpa) {
694 if (this->write_setup_register_(SEN6X_CMD_AMBIENT_PRESSURE, pressure_hpa))
695 this->last_ambient_pressure_ = pressure_hpa;
696}
697
699 if (std::isnan(pressure_hpa))
700 return;
701 // Range-check before narrowing so out-of-unit sources (e.g. Pa) can't wrap into range
702 if (pressure_hpa < AMBIENT_PRESSURE_MIN_HPA || pressure_hpa > AMBIENT_PRESSURE_MAX_HPA) {
703 if (!this->pressure_range_warned_) {
704 ESP_LOGW(TAG, "Ambient pressure out of range: %.0f hPa", pressure_hpa);
705 this->pressure_range_warned_ = true;
706 }
707 return;
708 }
709 this->pressure_range_warned_ = false;
710 const auto value = static_cast<uint16_t>(lroundf(pressure_hpa));
711 if (value != this->last_ambient_pressure_)
712 this->write_ambient_pressure_(value);
713}
714
715// Time left in the previous command's execution or settling window, 0 once it has passed
717 const uint32_t elapsed = millis() - this->command_started_at_;
718 return elapsed < this->command_wait_ ? this->command_wait_ - elapsed : 0;
719}
720
722
724 this->command_started_at_ = millis();
725 this->command_wait_ = wait_ms;
726}
727
729 this->measuring_ = true;
730 this->set_command_wait_(START_MEASUREMENT_DELAY);
731 // SEN63C/SEN69C condition the CO2 sensor for 24 s after a start; block restarts until then
732 if (this->sen6x_type_ == SEN63C || this->sen6x_type_ == SEN69C) {
733 this->co2_started_at_ = millis();
734 this->co2_wait_ = CO2_CONDITIONING_DELAY;
735 }
736 // Values need the warm-up period again after every start
737 this->startup_complete_ = false;
738 this->set_timeout(TIMEOUT_STARTUP, this->startup_delay_ms_, [this]() { this->startup_complete_ = true; });
739}
740
741void SEN6XComponent::start_measurement() {
742 if (!this->initialized_ || this->measuring_)
743 return;
744 // The CO2 conditioning window only blocks restarting, not other commands
746 ESP_LOGW(TAG, "Device busy");
747 return;
748 }
749 if (!this->write_command(SEN6X_CMD_START_MEASUREMENTS)) {
750 this->status_set_warning();
751 ESP_LOGW(TAG, "Start measurement failed (%d)", this->last_error_);
752 return;
753 }
755}
756
757void SEN6XComponent::stop_measurement() {
758 if (!this->initialized_ || !this->measuring_)
759 return;
760 if (this->command_blocked_()) {
761 ESP_LOGW(TAG, "Device busy");
762 return;
763 }
764 // Stop any in-flight polling before the device goes idle
765 this->cancel_timeout(TIMEOUT_POLL);
766 if (!this->write_command(SEN6X_CMD_STOP_MEASUREMENTS)) {
767 this->status_set_warning();
768 ESP_LOGW(TAG, "Stop measurement failed (%d)", this->last_error_);
769 return;
770 }
771 this->measuring_ = false;
772 this->set_command_wait_(STOP_MEASUREMENT_DELAY);
773}
774
775void SEN6XComponent::start_fan_cleaning() {
776 if (!this->initialized_)
777 return;
778 // The device only accepts this command in idle mode (datasheet section 4.8.22)
779 if (this->measuring_) {
780 ESP_LOGW(TAG, "Fan cleaning requires idle mode; stop the measurement first");
781 return;
782 }
783 if (this->command_blocked_()) {
784 ESP_LOGW(TAG, "Device busy");
785 return;
786 }
787 if (!this->write_command(SEN6X_CMD_START_FAN_CLEANING)) {
788 this->status_set_warning();
789 ESP_LOGW(TAG, "Fan cleaning failed (%d)", this->last_error_);
790 return;
791 }
792 this->set_command_wait_(FAN_CLEANING_DELAY);
793}
794
795void SEN6XComponent::activate_sht_heater() {
796 if (!this->initialized_)
797 return;
798 // The device only accepts this command in idle mode (datasheet section 4.8.23)
799 if (this->measuring_) {
800 ESP_LOGW(TAG, "SHT heater requires idle mode; stop the measurement first");
801 return;
802 }
803 if (this->command_blocked_()) {
804 ESP_LOGW(TAG, "Device busy");
805 return;
806 }
807 if (!this->write_command(SEN6X_CMD_ACTIVATE_SHT_HEATER)) {
808 this->status_set_warning();
809 ESP_LOGW(TAG, "SHT heater failed (%d)", this->last_error_);
810 return;
811 }
812 this->set_command_wait_(SHT_HEATER_DELAY);
813}
814
815SEN6XComponent::Sen6xType SEN6XComponent::infer_type_from_product_name_(const std::string &product_name) {
816 if (product_name == "SEN62")
817 return SEN62;
818 if (product_name == "SEN63C")
819 return SEN63C;
820 if (product_name == "SEN65")
821 return SEN65;
822 if (product_name == "SEN66")
823 return SEN66;
824 if (product_name == "SEN68")
825 return SEN68;
826 if (product_name == "SEN69C")
827 return SEN69C;
828 return UNKNOWN;
829}
830
831} // namespace esphome::sen6x
uint8_t status
Definition bl0942.h:8
void mark_failed()
Mark this component as failed.
virtual void setup()
Where the component's initialization should happen.
Definition component.cpp:84
bool status_comm_result(bool ok)
Clears the warning when ok, else sets it with message (default "Communication failed"); returns ok.
bool cancel_timeout(const char *name)
Cancel a timeout function.
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).
void parse_and_publish_number_concentration_()
Definition sen6x.cpp:676
bool write_temperature_compensation_(const TemperatureCompensation &compensation)
Definition sen6x.cpp:279
optional< GasTuning > voc_tuning_params_
Definition sen6x.h:175
bool has_number_concentration_sensor_() const
Definition sen6x.cpp:661
StaticVector< StatusSensor, SEN6X_STATUS_SENSOR_COUNT > status_sensors_
Definition sen6x.h:173
optional< TemperatureAcceleration > temperature_acceleration_
Definition sen6x.h:177
void set_command_wait_(uint32_t wait_ms)
Definition sen6x.cpp:723
uint32_t command_wait_remaining_() const
Definition sen6x.cpp:716
bool write_setup_register_(uint16_t i2c_command, uint16_t value)
Definition sen6x.cpp:291
sensor::Sensor * ambient_pressure_source_
Definition sen6x.h:180
bool write_temperature_acceleration_(const TemperatureAcceleration &acceleration)
Definition sen6x.cpp:286
Sen6xType infer_type_from_product_name_(const std::string &product_name)
Definition sen6x.cpp:815
bool write_config_words_(uint16_t i2c_command, const uint16_t *data, uint8_t len)
Definition sen6x.cpp:309
bool write_setup_step_(SetupStep step)
Definition sen6x.cpp:237
optional< TemperatureCompensation > temperature_compensation_
Definition sen6x.h:178
optional< GasTuning > nox_tuning_params_
Definition sen6x.h:176
optional< bool > co2_asc_
Definition sen6x.h:192
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning)
Definition sen6x.cpp:319
void write_ambient_pressure_(uint16_t pressure_hpa)
Definition sen6x.cpp:693
void update_ambient_pressure_compensation_(float pressure_hpa)
Definition sen6x.cpp:698
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...
Base-class for all sensors.
Definition sensor.h:47
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
uint8_t type
ESPHOME_LOG_TAG(TAG, "sen6x")
constexpr uint16_t ALTITUDE_UNSET
Definition sen6x.h:62
constexpr uint16_t PRESSURE_UNSET
Definition sen6x.h:63
const void size_t len
Definition hal.h:68
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
static void uint32_t
uint16_t learning_time_gain_hours
Definition sen6x.h:40
uint16_t learning_time_offset_hours
Definition sen6x.h:39
uint16_t gating_max_duration_minutes
Definition sen6x.h:41
uint16_t temperature
Definition sun_gtil2.cpp:12