ESPHome 2026.3.0-dev
Loading...
Searching...
No Matches
sen5x.cpp
Go to the documentation of this file.
1#include "sen5x.h"
3#include "esphome/core/hal.h"
5#include "esphome/core/log.h"
6#include <cinttypes>
7
8namespace esphome {
9namespace sen5x {
10
11static const char *const TAG = "sen5x";
12
13static const uint16_t SEN5X_CMD_AUTO_CLEANING_INTERVAL = 0x8004;
14static const uint16_t SEN5X_CMD_GET_DATA_READY_STATUS = 0x0202;
15static const uint16_t SEN5X_CMD_GET_FIRMWARE_VERSION = 0xD100;
16static const uint16_t SEN5X_CMD_GET_PRODUCT_NAME = 0xD014;
17static const uint16_t SEN5X_CMD_GET_SERIAL_NUMBER = 0xD033;
18static const uint16_t SEN5X_CMD_NOX_ALGORITHM_TUNING = 0x60E1;
19static const uint16_t SEN5X_CMD_READ_MEASUREMENT = 0x03C4;
20static const uint16_t SEN5X_CMD_RHT_ACCELERATION_MODE = 0x60F7;
21static const uint16_t SEN5X_CMD_START_CLEANING_FAN = 0x5607;
22static const uint16_t SEN5X_CMD_START_MEASUREMENTS = 0x0021;
23static const uint16_t SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY = 0x0037;
24static const uint16_t SEN5X_CMD_STOP_MEASUREMENTS = 0x3f86;
25static const uint16_t SEN5X_CMD_TEMPERATURE_COMPENSATION = 0x60B2;
26static const uint16_t SEN5X_CMD_VOC_ALGORITHM_STATE = 0x6181;
27static const uint16_t SEN5X_CMD_VOC_ALGORITHM_TUNING = 0x60D0;
28
29static const int8_t SEN5X_INDEX_SCALE_FACTOR = 10; // used for VOC and NOx index values
30static const int8_t SEN5X_MIN_INDEX_VALUE = 1 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor
31static const int16_t SEN5X_MAX_INDEX_VALUE = 500 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor
32
33static const LogString *type_to_string(Sen5xType type) {
34 switch (type) {
36 return LOG_STR("SEN50");
38 return LOG_STR("SEN54");
40 return LOG_STR("SEN55");
41 default:
42 return LOG_STR("UNKNOWN");
43 }
44}
45
46static const LogString *rht_accel_mode_to_string(RhtAccelerationMode mode) {
47 switch (mode) {
49 return LOG_STR("LOW");
51 return LOG_STR("MEDIUM");
53 return LOG_STR("HIGH");
54 default:
55 return LOG_STR("UNKNOWN");
56 }
57}
58
60 // the sensor needs 1000 ms to enter the idle state
61 this->set_timeout(1000, [this]() {
62 // Check if measurement is ready before reading the value
63 if (!this->write_command(SEN5X_CMD_GET_DATA_READY_STATUS)) {
64 ESP_LOGE(TAG, "Failed to write data ready status command");
65 this->mark_failed();
66 return;
67 }
68 delay(20); // per datasheet
69
70 uint16_t raw_read_status;
71 if (!this->read_data(raw_read_status)) {
72 ESP_LOGE(TAG, "Failed to read data ready status");
73 this->mark_failed();
74 return;
75 }
76
77 uint32_t stop_measurement_delay = 0;
78 // In order to query the device periodic measurement must be ceased
79 if (raw_read_status) {
80 ESP_LOGD(TAG, "Data is available; stopping periodic measurement");
81 if (!this->write_command(SEN5X_CMD_STOP_MEASUREMENTS)) {
82 ESP_LOGE(TAG, "Failed to stop measurements");
83 this->mark_failed();
84 return;
85 }
86 // According to the SEN5x datasheet the sensor will only respond to other commands after waiting 200 ms after
87 // issuing the stop_periodic_measurement command
88 stop_measurement_delay = 200;
89 }
90 this->set_timeout(stop_measurement_delay, [this]() {
91 // note: serial number register is actually 32-bytes long but we grab only the first 16-bytes,
92 // this appears to be all that Sensirion uses for serial numbers, this could change
93 uint16_t raw_serial_number[8];
94 if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 8, 20)) {
95 ESP_LOGE(TAG, "Failed to read serial number");
97 this->mark_failed();
98 return;
99 }
100 const char *serial_number = sensirion_convert_to_string_in_place(raw_serial_number, 8);
101 snprintf(this->serial_number_, sizeof(this->serial_number_), "%s", serial_number);
102 ESP_LOGV(TAG, "Serial number %s", this->serial_number_);
103
104 uint16_t raw_product_name[16];
105 if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) {
106 ESP_LOGE(TAG, "Failed to read product name");
108 this->mark_failed();
109 return;
110 }
111 const char *product_name = sensirion_convert_to_string_in_place(raw_product_name, 16);
112 if (strncmp(product_name, "SEN50", 5) == 0) {
113 this->type_ = Sen5xType::SEN50;
114 } else if (strncmp(product_name, "SEN54", 5) == 0) {
115 this->type_ = Sen5xType::SEN54;
116 } else if (strncmp(product_name, "SEN55", 5) == 0) {
117 this->type_ = Sen5xType::SEN55;
118 } else {
120 ESP_LOGE(TAG, "Unknown product name: %.32s", product_name);
122 this->mark_failed();
123 return;
124 }
125
126 ESP_LOGD(TAG, "Type: %s", LOG_STR_ARG(type_to_string(this->type_)));
127 if (this->humidity_sensor_ && this->type_ == Sen5xType::SEN50) {
128 ESP_LOGE(TAG, "Relative humidity requires a SEN54 or SEN55");
129 this->humidity_sensor_ = nullptr; // mark as not used
130 }
131 if (this->temperature_sensor_ && this->type_ == Sen5xType::SEN50) {
132 ESP_LOGE(TAG, "Temperature requires a SEN54 or SEN55");
133 this->temperature_sensor_ = nullptr; // mark as not used
134 }
135 if (this->voc_sensor_ && this->type_ == Sen5xType::SEN50) {
136 ESP_LOGE(TAG, "VOC requires a SEN54 or SEN55");
137 this->voc_sensor_ = nullptr; // mark as not used
138 }
139 if (this->nox_sensor_ && this->type_ != Sen5xType::SEN55) {
140 ESP_LOGE(TAG, "NOx requires a SEN55");
141 this->nox_sensor_ = nullptr; // mark as not used
142 }
143
144 if (!this->get_register(SEN5X_CMD_GET_FIRMWARE_VERSION, this->firmware_version_, 20)) {
145 ESP_LOGE(TAG, "Failed to read firmware version");
147 this->mark_failed();
148 return;
149 }
150 this->firmware_version_ >>= 8;
151 ESP_LOGV(TAG, "Firmware version %d", this->firmware_version_);
152
153 if (this->voc_sensor_ && this->store_baseline_) {
154 // Hash with serial number, serial numbers are unique, so multiple sensors can be used without conflict
155 uint32_t hash = fnv1a_hash(this->serial_number_);
156 this->pref_ = global_preferences->make_preference<uint16_t[4]>(hash, true);
158 if (this->pref_.load(&this->voc_baseline_state_)) {
159 if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, this->voc_baseline_state_, 4)) {
160 ESP_LOGE(TAG, "VOC Baseline State write to sensor failed");
161 } else {
162 ESP_LOGV(TAG, "VOC Baseline State loaded");
163 delay(20);
164 }
165 }
166 }
167 bool result;
169 // override default value
170 result = this->write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL, this->auto_cleaning_interval_.value());
171 } else {
172 result = this->write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL);
173 }
174 if (result) {
175 delay(20);
176 uint16_t secs[2];
177 if (this->read_data(secs, 2)) {
178 this->auto_cleaning_interval_ = secs[0] << 16 | secs[1];
179 }
180 }
181 if (this->acceleration_mode_.has_value()) {
182 result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE, this->acceleration_mode_.value());
183 } else {
184 result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE);
185 }
186 if (!result) {
187 ESP_LOGE(TAG, "Failed to set rh/t acceleration mode");
189 this->mark_failed();
190 return;
191 }
192 delay(20);
193 if (!this->acceleration_mode_.has_value()) {
194 uint16_t mode;
195 if (this->read_data(mode)) {
197 } else {
198 ESP_LOGE(TAG, "Failed to read RHT Acceleration mode");
199 }
200 }
201 if (this->voc_tuning_params_.has_value()) {
202 this->write_tuning_parameters_(SEN5X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value());
203 delay(20);
204 }
205 if (this->nox_tuning_params_.has_value()) {
206 this->write_tuning_parameters_(SEN5X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value());
207 delay(20);
208 }
209
210 if (this->temperature_compensation_.has_value()) {
212 delay(20);
213 }
214
215 // Finally start sensor measurements
216 auto cmd = SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY;
217 if (this->pm_1_0_sensor_ || this->pm_2_5_sensor_ || this->pm_4_0_sensor_ || this->pm_10_0_sensor_) {
218 // if any of the gas sensors are active we need a full measurement
219 cmd = SEN5X_CMD_START_MEASUREMENTS;
220 }
221
222 if (!this->write_command(cmd)) {
223 ESP_LOGE(TAG, "Error starting continuous measurements");
225 this->mark_failed();
226 return;
227 }
228 this->initialized_ = true;
229 });
230 });
231}
232
234 ESP_LOGCONFIG(TAG, "SEN5X:");
235 LOG_I2C_DEVICE(this);
236 if (this->is_failed()) {
237 switch (this->error_code_) {
239 ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
240 break;
242 ESP_LOGW(TAG, "Measurement initialization failed");
243 break;
245 ESP_LOGW(TAG, "Unable to read serial ID");
246 break;
248 ESP_LOGW(TAG, "Unable to read product name");
249 break;
250 case FIRMWARE_FAILED:
251 ESP_LOGW(TAG, "Unable to read firmware version");
252 break;
253 default:
254 ESP_LOGW(TAG, "Unknown setup error");
255 break;
256 }
257 }
258 ESP_LOGCONFIG(TAG,
259 " Type: %s\n"
260 " Firmware version: %d\n"
261 " Serial number: %s",
262 LOG_STR_ARG(type_to_string(this->type_)), this->firmware_version_, this->serial_number_);
264 ESP_LOGCONFIG(TAG, " Auto cleaning interval: %" PRId32 "s", this->auto_cleaning_interval_.value());
265 }
266 if (this->acceleration_mode_.has_value()) {
267 ESP_LOGCONFIG(TAG, " RH/T acceleration mode: %s",
268 LOG_STR_ARG(rht_accel_mode_to_string(this->acceleration_mode_.value())));
269 }
270 if (this->voc_sensor_) {
271 char hex_buf[5 * 4];
272 format_hex_pretty_to(hex_buf, this->voc_baseline_state_, 4, 0);
273 ESP_LOGCONFIG(TAG,
274 " Store Baseline: %s\n"
275 " State: %s\n",
276 TRUEFALSE(this->store_baseline_), hex_buf);
277 }
278 LOG_UPDATE_INTERVAL(this);
279 LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_);
280 LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_);
281 LOG_SENSOR(" ", "PM 4.0", this->pm_4_0_sensor_);
282 LOG_SENSOR(" ", "PM 10.0", this->pm_10_0_sensor_);
283 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
284 LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
285 LOG_SENSOR(" ", "VOC", this->voc_sensor_); // SEN54 and SEN55 only
286 LOG_SENSOR(" ", "NOx", this->nox_sensor_); // SEN55 only
287}
288
290 if (!this->initialized_) {
291 return;
292 }
293
294 if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) {
295 this->status_set_warning();
296 ESP_LOGD(TAG, "Write error: read measurement (%d)", this->last_error_);
297 return;
298 }
299 this->set_timeout(20, [this]() {
300 uint16_t measurements[8];
301
302 if (!this->read_data(measurements, 8)) {
303 this->status_set_warning();
304 ESP_LOGD(TAG, "Read data error (%d)", this->last_error_);
305 return;
306 }
307
308 ESP_LOGVV(TAG, "pm_1_0 = 0x%.4x", measurements[0]);
309 float pm_1_0 = measurements[0] == UINT16_MAX ? NAN : measurements[0] / 10.0f;
310
311 ESP_LOGVV(TAG, "pm_2_5 = 0x%.4x", measurements[1]);
312 float pm_2_5 = measurements[1] == UINT16_MAX ? NAN : measurements[1] / 10.0f;
313
314 ESP_LOGVV(TAG, "pm_4_0 = 0x%.4x", measurements[2]);
315 float pm_4_0 = measurements[2] == UINT16_MAX ? NAN : measurements[2] / 10.0f;
316
317 ESP_LOGVV(TAG, "pm_10_0 = 0x%.4x", measurements[3]);
318 float pm_10_0 = measurements[3] == UINT16_MAX ? NAN : measurements[3] / 10.0f;
319
320 ESP_LOGVV(TAG, "humidity = 0x%.4x", measurements[4]);
321 float humidity = measurements[4] == INT16_MAX ? NAN : static_cast<int16_t>(measurements[4]) / 100.0f;
322
323 ESP_LOGVV(TAG, "temperature = 0x%.4x", measurements[5]);
324 float temperature = measurements[5] == INT16_MAX ? NAN : static_cast<int16_t>(measurements[5]) / 200.0f;
325
326 ESP_LOGVV(TAG, "voc = 0x%.4x", measurements[6]);
327 int16_t voc_idx = static_cast<int16_t>(measurements[6]);
328 float voc = (voc_idx < SEN5X_MIN_INDEX_VALUE || voc_idx > SEN5X_MAX_INDEX_VALUE)
329 ? NAN
330 : static_cast<float>(voc_idx) / 10.0f;
331
332 ESP_LOGVV(TAG, "nox = 0x%.4x", measurements[7]);
333 int16_t nox_idx = static_cast<int16_t>(measurements[7]);
334 float nox = (nox_idx < SEN5X_MIN_INDEX_VALUE || nox_idx > SEN5X_MAX_INDEX_VALUE)
335 ? NAN
336 : static_cast<float>(nox_idx) / 10.0f;
337
338 if (this->pm_1_0_sensor_ != nullptr) {
339 this->pm_1_0_sensor_->publish_state(pm_1_0);
340 }
341 if (this->pm_2_5_sensor_ != nullptr) {
342 this->pm_2_5_sensor_->publish_state(pm_2_5);
343 }
344 if (this->pm_4_0_sensor_ != nullptr) {
345 this->pm_4_0_sensor_->publish_state(pm_4_0);
346 }
347 if (this->pm_10_0_sensor_ != nullptr) {
348 this->pm_10_0_sensor_->publish_state(pm_10_0);
349 }
350 if (this->temperature_sensor_ != nullptr) {
351 this->temperature_sensor_->publish_state(temperature);
352 }
353 if (this->humidity_sensor_ != nullptr) {
354 this->humidity_sensor_->publish_state(humidity);
355 }
356 if (this->voc_sensor_ != nullptr) {
357 this->voc_sensor_->publish_state(voc);
358 }
359 if (this->nox_sensor_ != nullptr) {
360 this->nox_sensor_->publish_state(nox);
361 }
362
363 if (!this->voc_sensor_ || !this->store_baseline_ ||
364 (App.get_loop_component_start_time() - this->voc_baseline_time_) < SHORTEST_BASELINE_STORE_INTERVAL) {
365 this->status_clear_warning();
366 } else {
368 if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) {
369 this->status_set_warning();
370 ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
371 } else {
372 this->set_timeout(20, [this]() {
373 if (!this->read_data(this->voc_baseline_state_, 4)) {
374 this->status_set_warning();
375 ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
376 } else {
377 if (this->pref_.save(&this->voc_baseline_state_)) {
378 ESP_LOGD(TAG, "VOC Baseline State saved");
379 }
380 this->status_clear_warning();
381 }
382 });
383 }
384 }
385 });
386}
387
388bool SEN5XComponent::write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning) {
389 uint16_t params[6];
390 params[0] = tuning.index_offset;
391 params[1] = tuning.learning_time_offset_hours;
392 params[2] = tuning.learning_time_gain_hours;
393 params[3] = tuning.gating_max_duration_minutes;
394 params[4] = tuning.std_initial;
395 params[5] = tuning.gain_factor;
396 auto result = write_command(i2c_command, params, 6);
397 if (!result) {
398 ESP_LOGE(TAG, "Set tuning parameters failed (command=%0xX, err=%d)", i2c_command, this->last_error_);
399 }
400 return result;
401}
402
404 uint16_t params[3];
405 params[0] = compensation.offset;
406 params[1] = compensation.normalized_offset_slope;
407 params[2] = compensation.time_constant;
408 if (!write_command(SEN5X_CMD_TEMPERATURE_COMPENSATION, params, 3)) {
409 ESP_LOGE(TAG, "Set temperature_compensation failed (%d)", this->last_error_);
410 return false;
411 }
412 return true;
413}
414
416 if (!write_command(SEN5X_CMD_START_CLEANING_FAN)) {
417 this->status_set_warning();
418 ESP_LOGE(TAG, "Start fan cleaning failed (%d)", this->last_error_);
419 return false;
420 } else {
421 ESP_LOGD(TAG, "Fan auto clean started");
422 }
423 return true;
424}
425
426} // namespace sen5x
427} // namespace esphome
BedjetMode mode
BedJet operating mode.
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void mark_failed()
Mark this component as failed.
bool is_failed() const
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()
bool save(const T *src)
Definition preferences.h:21
virtual ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash)=0
bool has_value() const
Definition optional.h:92
value_type const & value() const
Definition optional.h:94
optional< RhtAccelerationMode > acceleration_mode_
Definition sen5x.h:125
sensor::Sensor * pm_4_0_sensor_
Definition sen5x.h:116
void dump_config() override
Definition sen5x.cpp:233
ESPPreferenceObject pref_
Definition sen5x.h:130
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning)
Definition sen5x.cpp:388
sensor::Sensor * pm_2_5_sensor_
Definition sen5x.h:115
sensor::Sensor * temperature_sensor_
Definition sen5x.h:119
optional< uint32_t > auto_cleaning_interval_
Definition sen5x.h:126
sensor::Sensor * pm_1_0_sensor_
Definition sen5x.h:114
sensor::Sensor * voc_sensor_
Definition sen5x.h:121
optional< TemperatureCompensation > temperature_compensation_
Definition sen5x.h:129
sensor::Sensor * nox_sensor_
Definition sen5x.h:123
optional< GasTuning > voc_tuning_params_
Definition sen5x.h:127
sensor::Sensor * pm_10_0_sensor_
Definition sen5x.h:117
sensor::Sensor * humidity_sensor_
Definition sen5x.h:120
bool write_temperature_compensation_(const TemperatureCompensation &compensation)
Definition sen5x.cpp:403
optional< GasTuning > nox_tuning_params_
Definition sen5x.h:128
uint16_t voc_baseline_state_[4]
Definition sen5x.h:106
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...
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:65
uint16_t type
@ PRODUCT_NAME_FAILED
Definition sen5x.h:16
@ MEASUREMENT_INIT_FAILED
Definition sen5x.h:15
@ FIRMWARE_FAILED
Definition sen5x.h:17
@ SERIAL_NUMBER_IDENTIFICATION_FAILED
Definition sen5x.h:14
@ COMMUNICATION_FAILED
Definition sen5x.h:13
RhtAccelerationMode
Definition sen5x.h:21
@ LOW_ACCELERATION
Definition sen5x.h:22
@ HIGH_ACCELERATION
Definition sen5x.h:24
@ MEDIUM_ACCELERATION
Definition sen5x.h:23
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
Definition helpers.cpp:353
ESPPreferences * global_preferences
void HOT delay(uint32_t ms)
Definition core.cpp:27
Application App
Global storage of Application pointer - only one Application can exist.
constexpr uint32_t fnv1a_hash(const char *str)
Calculate a FNV-1a hash of str.
Definition helpers.h:599
uint16_t learning_time_gain_hours
Definition sen5x.h:32
uint16_t gating_max_duration_minutes
Definition sen5x.h:33
uint16_t learning_time_offset_hours
Definition sen5x.h:31
uint16_t temperature
Definition sun_gtil2.cpp:12
char serial_number[10]
Definition sun_gtil2.cpp:15