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