ESPHome 2026.1.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 *rht_accel_mode_to_string(RhtAccelerationMode mode) {
34 switch (mode) {
36 return LOG_STR("LOW");
38 return LOG_STR("MEDIUM");
40 return LOG_STR("HIGH");
41 default:
42 return LOG_STR("UNKNOWN");
43 }
44}
45
47 // the sensor needs 1000 ms to enter the idle state
48 this->set_timeout(1000, [this]() {
49 // Check if measurement is ready before reading the value
50 if (!this->write_command(SEN5X_CMD_GET_DATA_READY_STATUS)) {
51 ESP_LOGE(TAG, "Failed to write data ready status command");
52 this->mark_failed();
53 return;
54 }
55 delay(20); // per datasheet
56
57 uint16_t raw_read_status;
58 if (!this->read_data(raw_read_status)) {
59 ESP_LOGE(TAG, "Failed to read data ready status");
60 this->mark_failed();
61 return;
62 }
63
64 uint32_t stop_measurement_delay = 0;
65 // In order to query the device periodic measurement must be ceased
66 if (raw_read_status) {
67 ESP_LOGD(TAG, "Data is available; stopping periodic measurement");
68 if (!this->write_command(SEN5X_CMD_STOP_MEASUREMENTS)) {
69 ESP_LOGE(TAG, "Failed to stop measurements");
70 this->mark_failed();
71 return;
72 }
73 // According to the SEN5x datasheet the sensor will only respond to other commands after waiting 200 ms after
74 // issuing the stop_periodic_measurement command
75 stop_measurement_delay = 200;
76 }
77 this->set_timeout(stop_measurement_delay, [this]() {
78 uint16_t raw_serial_number[3];
79 if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 20)) {
80 ESP_LOGE(TAG, "Failed to read serial number");
82 this->mark_failed();
83 return;
84 }
85 this->serial_number_[0] = static_cast<bool>(uint16_t(raw_serial_number[0]) & 0xFF);
86 this->serial_number_[1] = static_cast<uint16_t>(raw_serial_number[0] & 0xFF);
87 this->serial_number_[2] = static_cast<uint16_t>(raw_serial_number[1] >> 8);
88 ESP_LOGV(TAG, "Serial number %02d.%02d.%02d", this->serial_number_[0], this->serial_number_[1],
89 this->serial_number_[2]);
90
91 uint16_t raw_product_name[16];
92 if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) {
93 ESP_LOGE(TAG, "Failed to read product name");
95 this->mark_failed();
96 return;
97 }
98 // 2 ASCII bytes are encoded in an int
99 const uint16_t *current_int = raw_product_name;
100 char current_char;
101 uint8_t max = 16;
102 do {
103 // first char
104 current_char = *current_int >> 8;
105 if (current_char) {
106 this->product_name_.push_back(current_char);
107 // second char
108 current_char = *current_int & 0xFF;
109 if (current_char) {
110 this->product_name_.push_back(current_char);
111 }
112 }
113 current_int++;
114 } while (current_char && --max);
115
116 Sen5xType sen5x_type = UNKNOWN;
117 if (this->product_name_ == "SEN50") {
118 sen5x_type = SEN50;
119 } else {
120 if (this->product_name_ == "SEN54") {
121 sen5x_type = SEN54;
122 } else {
123 if (this->product_name_ == "SEN55") {
124 sen5x_type = SEN55;
125 }
126 }
127 ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str());
128 }
129 if (this->humidity_sensor_ && sen5x_type == SEN50) {
130 ESP_LOGE(TAG, "Relative humidity requires a SEN54 or SEN55");
131 this->humidity_sensor_ = nullptr; // mark as not used
132 }
133 if (this->temperature_sensor_ && sen5x_type == SEN50) {
134 ESP_LOGE(TAG, "Temperature requires a SEN54 or SEN55");
135 this->temperature_sensor_ = nullptr; // mark as not used
136 }
137 if (this->voc_sensor_ && sen5x_type == SEN50) {
138 ESP_LOGE(TAG, "VOC requires a SEN54 or SEN55");
139 this->voc_sensor_ = nullptr; // mark as not used
140 }
141 if (this->nox_sensor_ && sen5x_type != SEN55) {
142 ESP_LOGE(TAG, "NOx requires a SEN55");
143 this->nox_sensor_ = nullptr; // mark as not used
144 }
145
146 if (!this->get_register(SEN5X_CMD_GET_FIRMWARE_VERSION, this->firmware_version_, 20)) {
147 ESP_LOGE(TAG, "Failed to read firmware version");
149 this->mark_failed();
150 return;
151 }
152 this->firmware_version_ >>= 8;
153 ESP_LOGV(TAG, "Firmware version %d", this->firmware_version_);
154
155 if (this->voc_sensor_ && this->store_baseline_) {
156 uint32_t combined_serial =
157 encode_uint24(this->serial_number_[0], this->serial_number_[1], this->serial_number_[2]);
158 // Hash with config hash, version, and serial number
159 // This ensures the baseline storage is cleared after OTA
160 // Serial numbers are unique to each sensor, so multiple sensors can be used without conflict
161 uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), std::to_string(combined_serial));
163
164 if (this->pref_.load(&this->voc_baselines_storage_)) {
165 ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
166 this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
167 }
168
169 // Initialize storage timestamp
171
172 if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) {
173 ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
174 this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
175 uint16_t states[4];
176
177 states[0] = this->voc_baselines_storage_.state0 >> 16;
178 states[1] = this->voc_baselines_storage_.state0 & 0xFFFF;
179 states[2] = this->voc_baselines_storage_.state1 >> 16;
180 states[3] = this->voc_baselines_storage_.state1 & 0xFFFF;
181
182 if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) {
183 ESP_LOGE(TAG, "Failed to set VOC baseline from saved state");
184 }
185 }
186 }
187 bool result;
189 // override default value
190 result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL, this->auto_cleaning_interval_.value());
191 } else {
192 result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL);
193 }
194 if (result) {
195 delay(20);
196 uint16_t secs[2];
197 if (this->read_data(secs, 2)) {
198 this->auto_cleaning_interval_ = secs[0] << 16 | secs[1];
199 }
200 }
201 if (this->acceleration_mode_.has_value()) {
202 result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE, this->acceleration_mode_.value());
203 } else {
204 result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE);
205 }
206 if (!result) {
207 ESP_LOGE(TAG, "Failed to set rh/t acceleration mode");
209 this->mark_failed();
210 return;
211 }
212 delay(20);
213 if (!this->acceleration_mode_.has_value()) {
214 uint16_t mode;
215 if (this->read_data(mode)) {
217 } else {
218 ESP_LOGE(TAG, "Failed to read RHT Acceleration mode");
219 }
220 }
221 if (this->voc_tuning_params_.has_value()) {
222 this->write_tuning_parameters_(SEN5X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value());
223 delay(20);
224 }
225 if (this->nox_tuning_params_.has_value()) {
226 this->write_tuning_parameters_(SEN5X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value());
227 delay(20);
228 }
229
230 if (this->temperature_compensation_.has_value()) {
232 delay(20);
233 }
234
235 // Finally start sensor measurements
236 auto cmd = SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY;
237 if (this->pm_1_0_sensor_ || this->pm_2_5_sensor_ || this->pm_4_0_sensor_ || this->pm_10_0_sensor_) {
238 // if any of the gas sensors are active we need a full measurement
239 cmd = SEN5X_CMD_START_MEASUREMENTS;
240 }
241
242 if (!this->write_command(cmd)) {
243 ESP_LOGE(TAG, "Error starting continuous measurements");
245 this->mark_failed();
246 return;
247 }
248 this->initialized_ = true;
249 });
250 });
251}
252
254 ESP_LOGCONFIG(TAG, "SEN5X:");
255 LOG_I2C_DEVICE(this);
256 if (this->is_failed()) {
257 switch (this->error_code_) {
259 ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
260 break;
262 ESP_LOGW(TAG, "Measurement initialization failed");
263 break;
265 ESP_LOGW(TAG, "Unable to read serial ID");
266 break;
268 ESP_LOGW(TAG, "Unable to read product name");
269 break;
270 case FIRMWARE_FAILED:
271 ESP_LOGW(TAG, "Unable to read firmware version");
272 break;
273 default:
274 ESP_LOGW(TAG, "Unknown setup error");
275 break;
276 }
277 }
278 ESP_LOGCONFIG(TAG,
279 " Product name: %s\n"
280 " Firmware version: %d\n"
281 " Serial number %02d.%02d.%02d",
282 this->product_name_.c_str(), this->firmware_version_, this->serial_number_[0], this->serial_number_[1],
283 this->serial_number_[2]);
285 ESP_LOGCONFIG(TAG, " Auto cleaning interval: %" PRId32 "s", this->auto_cleaning_interval_.value());
286 }
287 if (this->acceleration_mode_.has_value()) {
288 ESP_LOGCONFIG(TAG, " RH/T acceleration mode: %s",
289 LOG_STR_ARG(rht_accel_mode_to_string(this->acceleration_mode_.value())));
290 }
291 LOG_UPDATE_INTERVAL(this);
292 LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_);
293 LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_);
294 LOG_SENSOR(" ", "PM 4.0", this->pm_4_0_sensor_);
295 LOG_SENSOR(" ", "PM 10.0", this->pm_10_0_sensor_);
296 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
297 LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
298 LOG_SENSOR(" ", "VOC", this->voc_sensor_); // SEN54 and SEN55 only
299 LOG_SENSOR(" ", "NOx", this->nox_sensor_); // SEN55 only
300}
301
303 if (!this->initialized_) {
304 return;
305 }
306
307 // Store baselines after defined interval or if the difference between current and stored baseline becomes too
308 // much
309 if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
310 if (this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) {
311 // run it a bit later to avoid adding a delay here
312 this->set_timeout(550, [this]() {
313 uint16_t states[4];
314 if (this->read_data(states, 4)) {
315 uint32_t state0 = states[0] << 16 | states[1];
316 uint32_t state1 = states[2] << 16 | states[3];
317 if ((uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state0 - state0)) >
318 MAXIMUM_STORAGE_DIFF ||
319 (uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state1 - state1)) >
320 MAXIMUM_STORAGE_DIFF) {
322 this->voc_baselines_storage_.state0 = state0;
323 this->voc_baselines_storage_.state1 = state1;
324
325 if (this->pref_.save(&this->voc_baselines_storage_)) {
326 ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
327 this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
328 } else {
329 ESP_LOGW(TAG, "Could not store VOC baselines");
330 }
331 }
332 }
333 });
334 }
335 }
336
337 if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) {
338 this->status_set_warning();
339 ESP_LOGD(TAG, "Write error: read measurement (%d)", this->last_error_);
340 return;
341 }
342 this->set_timeout(20, [this]() {
343 uint16_t measurements[8];
344
345 if (!this->read_data(measurements, 8)) {
346 this->status_set_warning();
347 ESP_LOGD(TAG, "Read data error (%d)", this->last_error_);
348 return;
349 }
350
351 ESP_LOGVV(TAG, "pm_1_0 = 0x%.4x", measurements[0]);
352 float pm_1_0 = measurements[0] == UINT16_MAX ? NAN : measurements[0] / 10.0f;
353
354 ESP_LOGVV(TAG, "pm_2_5 = 0x%.4x", measurements[1]);
355 float pm_2_5 = measurements[1] == UINT16_MAX ? NAN : measurements[1] / 10.0f;
356
357 ESP_LOGVV(TAG, "pm_4_0 = 0x%.4x", measurements[2]);
358 float pm_4_0 = measurements[2] == UINT16_MAX ? NAN : measurements[2] / 10.0f;
359
360 ESP_LOGVV(TAG, "pm_10_0 = 0x%.4x", measurements[3]);
361 float pm_10_0 = measurements[3] == UINT16_MAX ? NAN : measurements[3] / 10.0f;
362
363 ESP_LOGVV(TAG, "humidity = 0x%.4x", measurements[4]);
364 float humidity = measurements[4] == INT16_MAX ? NAN : static_cast<int16_t>(measurements[4]) / 100.0f;
365
366 ESP_LOGVV(TAG, "temperature = 0x%.4x", measurements[5]);
367 float temperature = measurements[5] == INT16_MAX ? NAN : static_cast<int16_t>(measurements[5]) / 200.0f;
368
369 ESP_LOGVV(TAG, "voc = 0x%.4x", measurements[6]);
370 int16_t voc_idx = static_cast<int16_t>(measurements[6]);
371 float voc = (voc_idx < SEN5X_MIN_INDEX_VALUE || voc_idx > SEN5X_MAX_INDEX_VALUE)
372 ? NAN
373 : static_cast<float>(voc_idx) / 10.0f;
374
375 ESP_LOGVV(TAG, "nox = 0x%.4x", measurements[7]);
376 int16_t nox_idx = static_cast<int16_t>(measurements[7]);
377 float nox = (nox_idx < SEN5X_MIN_INDEX_VALUE || nox_idx > SEN5X_MAX_INDEX_VALUE)
378 ? NAN
379 : static_cast<float>(nox_idx) / 10.0f;
380
381 if (this->pm_1_0_sensor_ != nullptr) {
382 this->pm_1_0_sensor_->publish_state(pm_1_0);
383 }
384 if (this->pm_2_5_sensor_ != nullptr) {
385 this->pm_2_5_sensor_->publish_state(pm_2_5);
386 }
387 if (this->pm_4_0_sensor_ != nullptr) {
388 this->pm_4_0_sensor_->publish_state(pm_4_0);
389 }
390 if (this->pm_10_0_sensor_ != nullptr) {
391 this->pm_10_0_sensor_->publish_state(pm_10_0);
392 }
393 if (this->temperature_sensor_ != nullptr) {
394 this->temperature_sensor_->publish_state(temperature);
395 }
396 if (this->humidity_sensor_ != nullptr) {
397 this->humidity_sensor_->publish_state(humidity);
398 }
399 if (this->voc_sensor_ != nullptr) {
400 this->voc_sensor_->publish_state(voc);
401 }
402 if (this->nox_sensor_ != nullptr) {
403 this->nox_sensor_->publish_state(nox);
404 }
405 this->status_clear_warning();
406 });
407}
408
409bool SEN5XComponent::write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning) {
410 uint16_t params[6];
411 params[0] = tuning.index_offset;
412 params[1] = tuning.learning_time_offset_hours;
413 params[2] = tuning.learning_time_gain_hours;
414 params[3] = tuning.gating_max_duration_minutes;
415 params[4] = tuning.std_initial;
416 params[5] = tuning.gain_factor;
417 auto result = write_command(i2c_command, params, 6);
418 if (!result) {
419 ESP_LOGE(TAG, "Set tuning parameters failed (command=%0xX, err=%d)", i2c_command, this->last_error_);
420 }
421 return result;
422}
423
425 uint16_t params[3];
426 params[0] = compensation.offset;
427 params[1] = compensation.normalized_offset_slope;
428 params[2] = compensation.time_constant;
429 if (!write_command(SEN5X_CMD_TEMPERATURE_COMPENSATION, params, 3)) {
430 ESP_LOGE(TAG, "Set temperature_compensation failed (%d)", this->last_error_);
431 return false;
432 }
433 return true;
434}
435
437 if (!write_command(SEN5X_CMD_START_CLEANING_FAN)) {
438 this->status_set_warning();
439 ESP_LOGE(TAG, "Start fan cleaning failed (%d)", this->last_error_);
440 return false;
441 } else {
442 ESP_LOGD(TAG, "Fan auto clean started");
443 }
444 return true;
445}
446
447} // namespace sen5x
448} // namespace esphome
BedjetMode mode
BedJet operating mode.
constexpr uint32_t get_config_version_hash()
Get the config hash extended with ESPHome version.
virtual void mark_failed()
Mark this component as failed.
bool is_failed() const
void status_set_warning(const char *message=nullptr)
void status_clear_warning()
void set_timeout(const std::string &name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a unique name.
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:128
sensor::Sensor * pm_4_0_sensor_
Definition sen5x.h:119
void dump_config() override
Definition sen5x.cpp:253
ESPPreferenceObject pref_
Definition sen5x.h:133
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning)
Definition sen5x.cpp:409
sensor::Sensor * pm_2_5_sensor_
Definition sen5x.h:118
sensor::Sensor * temperature_sensor_
Definition sen5x.h:122
optional< uint32_t > auto_cleaning_interval_
Definition sen5x.h:129
sensor::Sensor * pm_1_0_sensor_
Definition sen5x.h:117
sensor::Sensor * voc_sensor_
Definition sen5x.h:124
optional< TemperatureCompensation > temperature_compensation_
Definition sen5x.h:132
sensor::Sensor * nox_sensor_
Definition sen5x.h:126
optional< GasTuning > voc_tuning_params_
Definition sen5x.h:130
sensor::Sensor * pm_10_0_sensor_
Definition sen5x.h:120
Sen5xBaselines voc_baselines_storage_
Definition sen5x.h:135
sensor::Sensor * humidity_sensor_
Definition sen5x.h:123
bool write_temperature_compensation_(const TemperatureCompensation &compensation)
Definition sen5x.cpp:424
optional< GasTuning > nox_tuning_params_
Definition sen5x.h:131
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.
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:77
@ 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
constexpr uint32_t fnv1a_hash_extend(uint32_t hash, const char *str)
Extend a FNV-1a hash with additional string data.
Definition helpers.h:391
constexpr uint32_t encode_uint24(uint8_t byte1, uint8_t byte2, uint8_t byte3)
Encode a 24-bit value given three bytes in most to least significant byte order.
Definition helpers.h:424
ESPPreferences * global_preferences
void IRAM_ATTR HOT delay(uint32_t ms)
Definition core.cpp:26
Application App
Global storage of Application pointer - only one Application can exist.
uint16_t learning_time_gain_hours
Definition sen5x.h:35
uint16_t gating_max_duration_minutes
Definition sen5x.h:36
uint16_t learning_time_offset_hours
Definition sen5x.h:34
uint16_t temperature
Definition sun_gtil2.cpp:12