ESPHome 2025.5.0
Loading...
Searching...
No Matches
sen5x.cpp
Go to the documentation of this file.
1#include "sen5x.h"
2#include "esphome/core/hal.h"
4#include "esphome/core/log.h"
5#include <cinttypes>
6
7namespace esphome {
8namespace sen5x {
9
10static const char *const TAG = "sen5x";
11
12static const uint16_t SEN5X_CMD_AUTO_CLEANING_INTERVAL = 0x8004;
13static const uint16_t SEN5X_CMD_GET_DATA_READY_STATUS = 0x0202;
14static const uint16_t SEN5X_CMD_GET_FIRMWARE_VERSION = 0xD100;
15static const uint16_t SEN5X_CMD_GET_PRODUCT_NAME = 0xD014;
16static const uint16_t SEN5X_CMD_GET_SERIAL_NUMBER = 0xD033;
17static const uint16_t SEN5X_CMD_NOX_ALGORITHM_TUNING = 0x60E1;
18static const uint16_t SEN5X_CMD_READ_MEASUREMENT = 0x03C4;
19static const uint16_t SEN5X_CMD_RHT_ACCELERATION_MODE = 0x60F7;
20static const uint16_t SEN5X_CMD_START_CLEANING_FAN = 0x5607;
21static const uint16_t SEN5X_CMD_START_MEASUREMENTS = 0x0021;
22static const uint16_t SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY = 0x0037;
23static const uint16_t SEN5X_CMD_STOP_MEASUREMENTS = 0x3f86;
24static const uint16_t SEN5X_CMD_TEMPERATURE_COMPENSATION = 0x60B2;
25static const uint16_t SEN5X_CMD_VOC_ALGORITHM_STATE = 0x6181;
26static const uint16_t SEN5X_CMD_VOC_ALGORITHM_TUNING = 0x60D0;
27
28static const int8_t SEN5X_INDEX_SCALE_FACTOR = 10; // used for VOC and NOx index values
29static const int8_t SEN5X_MIN_INDEX_VALUE = 1 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor
30static const int16_t SEN5X_MAX_INDEX_VALUE = 500 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor
31
33 ESP_LOGCONFIG(TAG, "Setting up sen5x...");
34
35 // the sensor needs 1000 ms to enter the idle state
36 this->set_timeout(1000, [this]() {
37 // Check if measurement is ready before reading the value
38 if (!this->write_command(SEN5X_CMD_GET_DATA_READY_STATUS)) {
39 ESP_LOGE(TAG, "Failed to write data ready status command");
40 this->mark_failed();
41 return;
42 }
43
44 uint16_t raw_read_status;
45 if (!this->read_data(raw_read_status)) {
46 ESP_LOGE(TAG, "Failed to read data ready status");
47 this->mark_failed();
48 return;
49 }
50
51 uint32_t stop_measurement_delay = 0;
52 // In order to query the device periodic measurement must be ceased
53 if (raw_read_status) {
54 ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement");
55 if (!this->write_command(SEN5X_CMD_STOP_MEASUREMENTS)) {
56 ESP_LOGE(TAG, "Failed to stop measurements");
57 this->mark_failed();
58 return;
59 }
60 // According to the SEN5x datasheet the sensor will only respond to other commands after waiting 200 ms after
61 // issuing the stop_periodic_measurement command
62 stop_measurement_delay = 200;
63 }
64 this->set_timeout(stop_measurement_delay, [this]() {
65 uint16_t raw_serial_number[3];
66 if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 20)) {
67 ESP_LOGE(TAG, "Failed to read serial number");
69 this->mark_failed();
70 return;
71 }
72 this->serial_number_[0] = static_cast<bool>(uint16_t(raw_serial_number[0]) & 0xFF);
73 this->serial_number_[1] = static_cast<uint16_t>(raw_serial_number[0] & 0xFF);
74 this->serial_number_[2] = static_cast<uint16_t>(raw_serial_number[1] >> 8);
75 ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]);
76
77 uint16_t raw_product_name[16];
78 if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) {
79 ESP_LOGE(TAG, "Failed to read product name");
81 this->mark_failed();
82 return;
83 }
84 // 2 ASCII bytes are encoded in an int
85 const uint16_t *current_int = raw_product_name;
86 char current_char;
87 uint8_t max = 16;
88 do {
89 // first char
90 current_char = *current_int >> 8;
91 if (current_char) {
92 product_name_.push_back(current_char);
93 // second char
94 current_char = *current_int & 0xFF;
95 if (current_char) {
96 product_name_.push_back(current_char);
97 }
98 }
99 current_int++;
100 } while (current_char && --max);
101
102 Sen5xType sen5x_type = UNKNOWN;
103 if (product_name_ == "SEN50") {
104 sen5x_type = SEN50;
105 } else {
106 if (product_name_ == "SEN54") {
107 sen5x_type = SEN54;
108 } else {
109 if (product_name_ == "SEN55") {
110 sen5x_type = SEN55;
111 }
112 }
113 ESP_LOGD(TAG, "Productname %s", product_name_.c_str());
114 }
115 if (this->humidity_sensor_ && sen5x_type == SEN50) {
116 ESP_LOGE(TAG, "For Relative humidity a SEN54 OR SEN55 is required. You are using a <%s> sensor",
117 this->product_name_.c_str());
118 this->humidity_sensor_ = nullptr; // mark as not used
119 }
120 if (this->temperature_sensor_ && sen5x_type == SEN50) {
121 ESP_LOGE(TAG, "For Temperature a SEN54 OR SEN55 is required. You are using a <%s> sensor",
122 this->product_name_.c_str());
123 this->temperature_sensor_ = nullptr; // mark as not used
124 }
125 if (this->voc_sensor_ && sen5x_type == SEN50) {
126 ESP_LOGE(TAG, "For VOC a SEN54 OR SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str());
127 this->voc_sensor_ = nullptr; // mark as not used
128 }
129 if (this->nox_sensor_ && sen5x_type != SEN55) {
130 ESP_LOGE(TAG, "For NOx a SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str());
131 this->nox_sensor_ = nullptr; // mark as not used
132 }
133
134 if (!this->get_register(SEN5X_CMD_GET_FIRMWARE_VERSION, this->firmware_version_, 20)) {
135 ESP_LOGE(TAG, "Failed to read firmware version");
137 this->mark_failed();
138 return;
139 }
140 this->firmware_version_ >>= 8;
141 ESP_LOGD(TAG, "Firmware version %d", this->firmware_version_);
142
143 if (this->voc_sensor_ && this->store_baseline_) {
144 uint32_t combined_serial =
145 encode_uint24(this->serial_number_[0], this->serial_number_[1], this->serial_number_[2]);
146 // Hash with compilation time and serial number
147 // This ensures the baseline storage is cleared after OTA
148 // Serial numbers are unique to each sensor, so mulitple sensors can be used without conflict
149 uint32_t hash = fnv1_hash(App.get_compilation_time() + std::to_string(combined_serial));
151
152 if (this->pref_.load(&this->voc_baselines_storage_)) {
153 ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
155 }
156
157 // Initialize storage timestamp
159
160 if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) {
161 ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
163 uint16_t states[4];
164
165 states[0] = voc_baselines_storage_.state0 >> 16;
166 states[1] = voc_baselines_storage_.state0 & 0xFFFF;
167 states[2] = voc_baselines_storage_.state1 >> 16;
168 states[3] = voc_baselines_storage_.state1 & 0xFFFF;
169
170 if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) {
171 ESP_LOGE(TAG, "Failed to set VOC baseline from saved state");
172 }
173 }
174 }
175 bool result;
177 // override default value
178 result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL, this->auto_cleaning_interval_.value());
179 } else {
180 result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL);
181 }
182 if (result) {
183 delay(20);
184 uint16_t secs[2];
185 if (this->read_data(secs, 2)) {
186 auto_cleaning_interval_ = secs[0] << 16 | secs[1];
187 }
188 }
190 result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE, acceleration_mode_.value());
191 } else {
192 result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE);
193 }
194 if (!result) {
195 ESP_LOGE(TAG, "Failed to set rh/t acceleration mode");
197 this->mark_failed();
198 return;
199 }
200 delay(20);
202 uint16_t mode;
203 if (this->read_data(mode)) {
205 } else {
206 ESP_LOGE(TAG, "Failed to read RHT Acceleration mode");
207 }
208 }
209 if (this->voc_tuning_params_.has_value()) {
210 this->write_tuning_parameters_(SEN5X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value());
211 delay(20);
212 }
213 if (this->nox_tuning_params_.has_value()) {
214 this->write_tuning_parameters_(SEN5X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value());
215 delay(20);
216 }
217
218 if (this->temperature_compensation_.has_value()) {
220 delay(20);
221 }
222
223 // Finally start sensor measurements
224 auto cmd = SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY;
225 if (this->pm_1_0_sensor_ || this->pm_2_5_sensor_ || this->pm_4_0_sensor_ || this->pm_10_0_sensor_) {
226 // if any of the gas sensors are active we need a full measurement
227 cmd = SEN5X_CMD_START_MEASUREMENTS;
228 }
229
230 if (!this->write_command(cmd)) {
231 ESP_LOGE(TAG, "Error starting continuous measurements.");
233 this->mark_failed();
234 return;
235 }
236 initialized_ = true;
237 ESP_LOGD(TAG, "Sensor initialized");
238 });
239 });
240}
241
243 ESP_LOGCONFIG(TAG, "sen5x:");
244 LOG_I2C_DEVICE(this);
245 if (this->is_failed()) {
246 switch (this->error_code_) {
248 ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
249 break;
251 ESP_LOGW(TAG, "Measurement Initialization failed!");
252 break;
254 ESP_LOGW(TAG, "Unable to read sensor serial id");
255 break;
257 ESP_LOGW(TAG, "Unable to read product name");
258 break;
259 case FIRMWARE_FAILED:
260 ESP_LOGW(TAG, "Unable to read sensor firmware version");
261 break;
262 default:
263 ESP_LOGW(TAG, "Unknown setup error!");
264 break;
265 }
266 }
267 ESP_LOGCONFIG(TAG, " Productname: %s", this->product_name_.c_str());
268 ESP_LOGCONFIG(TAG, " Firmware version: %d", this->firmware_version_);
269 ESP_LOGCONFIG(TAG, " Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]);
271 ESP_LOGCONFIG(TAG, " Auto cleaning interval %" PRId32 " seconds", auto_cleaning_interval_.value());
272 }
273 if (this->acceleration_mode_.has_value()) {
274 switch (this->acceleration_mode_.value()) {
275 case LOW_ACCELERATION:
276 ESP_LOGCONFIG(TAG, " Low RH/T acceleration mode");
277 break;
279 ESP_LOGCONFIG(TAG, " Medium RH/T acceleration mode");
280 break;
282 ESP_LOGCONFIG(TAG, " High RH/T acceleration mode");
283 break;
284 }
285 }
286 LOG_UPDATE_INTERVAL(this);
287 LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_);
288 LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_);
289 LOG_SENSOR(" ", "PM 4.0", this->pm_4_0_sensor_);
290 LOG_SENSOR(" ", "PM 10.0", this->pm_10_0_sensor_);
291 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
292 LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
293 LOG_SENSOR(" ", "VOC", this->voc_sensor_); // SEN54 and SEN55 only
294 LOG_SENSOR(" ", "NOx", this->nox_sensor_); // SEN55 only
295}
296
298 if (!initialized_) {
299 return;
300 }
301
302 // Store baselines after defined interval or if the difference between current and stored baseline becomes too
303 // much
305 if (this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) {
306 // run it a bit later to avoid adding a delay here
307 this->set_timeout(550, [this]() {
308 uint16_t states[4];
309 if (this->read_data(states, 4)) {
310 uint32_t state0 = states[0] << 16 | states[1];
311 uint32_t state1 = states[2] << 16 | states[3];
312 if ((uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state0 - state0)) >
314 (uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state1 - state1)) >
317 this->voc_baselines_storage_.state0 = state0;
318 this->voc_baselines_storage_.state1 = state1;
319
320 if (this->pref_.save(&this->voc_baselines_storage_)) {
321 ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 " ,state1: 0x%04" PRIX32,
323 } else {
324 ESP_LOGW(TAG, "Could not store VOC baselines");
325 }
326 }
327 }
328 });
329 }
330 }
331
332 if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) {
333 this->status_set_warning();
334 ESP_LOGD(TAG, "write error read measurement (%d)", this->last_error_);
335 return;
336 }
337 this->set_timeout(20, [this]() {
338 uint16_t measurements[8];
339
340 if (!this->read_data(measurements, 8)) {
341 this->status_set_warning();
342 ESP_LOGD(TAG, "read data error (%d)", this->last_error_);
343 return;
344 }
345
346 ESP_LOGVV(TAG, "pm_1_0 = 0x%.4x", measurements[0]);
347 float pm_1_0 = measurements[0] == UINT16_MAX ? NAN : measurements[0] / 10.0f;
348
349 ESP_LOGVV(TAG, "pm_2_5 = 0x%.4x", measurements[1]);
350 float pm_2_5 = measurements[1] == UINT16_MAX ? NAN : measurements[1] / 10.0f;
351
352 ESP_LOGVV(TAG, "pm_4_0 = 0x%.4x", measurements[2]);
353 float pm_4_0 = measurements[2] == UINT16_MAX ? NAN : measurements[2] / 10.0f;
354
355 ESP_LOGVV(TAG, "pm_10_0 = 0x%.4x", measurements[3]);
356 float pm_10_0 = measurements[3] == UINT16_MAX ? NAN : measurements[3] / 10.0f;
357
358 ESP_LOGVV(TAG, "humidity = 0x%.4x", measurements[4]);
359 float humidity = measurements[4] == INT16_MAX ? NAN : static_cast<int16_t>(measurements[4]) / 100.0f;
360
361 ESP_LOGVV(TAG, "temperature = 0x%.4x", measurements[5]);
362 float temperature = measurements[5] == INT16_MAX ? NAN : static_cast<int16_t>(measurements[5]) / 200.0f;
363
364 ESP_LOGVV(TAG, "voc = 0x%.4x", measurements[6]);
365 int16_t voc_idx = static_cast<int16_t>(measurements[6]);
366 float voc = (voc_idx < SEN5X_MIN_INDEX_VALUE || voc_idx > SEN5X_MAX_INDEX_VALUE)
367 ? NAN
368 : static_cast<float>(voc_idx) / 10.0f;
369
370 ESP_LOGVV(TAG, "nox = 0x%.4x", measurements[7]);
371 int16_t nox_idx = static_cast<int16_t>(measurements[7]);
372 float nox = (nox_idx < SEN5X_MIN_INDEX_VALUE || nox_idx > SEN5X_MAX_INDEX_VALUE)
373 ? NAN
374 : static_cast<float>(nox_idx) / 10.0f;
375
376 if (this->pm_1_0_sensor_ != nullptr) {
377 this->pm_1_0_sensor_->publish_state(pm_1_0);
378 }
379 if (this->pm_2_5_sensor_ != nullptr) {
380 this->pm_2_5_sensor_->publish_state(pm_2_5);
381 }
382 if (this->pm_4_0_sensor_ != nullptr) {
383 this->pm_4_0_sensor_->publish_state(pm_4_0);
384 }
385 if (this->pm_10_0_sensor_ != nullptr) {
386 this->pm_10_0_sensor_->publish_state(pm_10_0);
387 }
388 if (this->temperature_sensor_ != nullptr) {
389 this->temperature_sensor_->publish_state(temperature);
390 }
391 if (this->humidity_sensor_ != nullptr) {
392 this->humidity_sensor_->publish_state(humidity);
393 }
394 if (this->voc_sensor_ != nullptr) {
395 this->voc_sensor_->publish_state(voc);
396 }
397 if (this->nox_sensor_ != nullptr) {
398 this->nox_sensor_->publish_state(nox);
399 }
400 this->status_clear_warning();
401 });
402}
403
404bool SEN5XComponent::write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning) {
405 uint16_t params[6];
406 params[0] = tuning.index_offset;
407 params[1] = tuning.learning_time_offset_hours;
408 params[2] = tuning.learning_time_gain_hours;
409 params[3] = tuning.gating_max_duration_minutes;
410 params[4] = tuning.std_initial;
411 params[5] = tuning.gain_factor;
412 auto result = write_command(i2c_command, params, 6);
413 if (!result) {
414 ESP_LOGE(TAG, "set tuning parameters failed. i2c command=%0xX, err=%d", i2c_command, this->last_error_);
415 }
416 return result;
417}
418
420 uint16_t params[3];
421 params[0] = compensation.offset;
422 params[1] = compensation.normalized_offset_slope;
423 params[2] = compensation.time_constant;
424 if (!write_command(SEN5X_CMD_TEMPERATURE_COMPENSATION, params, 3)) {
425 ESP_LOGE(TAG, "set temperature_compensation failed. Err=%d", this->last_error_);
426 return false;
427 }
428 return true;
429}
430
432 if (!write_command(SEN5X_CMD_START_CLEANING_FAN)) {
433 this->status_set_warning();
434 ESP_LOGE(TAG, "write error start fan (%d)", this->last_error_);
435 return false;
436 } else {
437 ESP_LOGD(TAG, "Fan auto clean started");
438 }
439 return true;
440}
441
442} // namespace sen5x
443} // namespace esphome
BedjetMode mode
BedJet operating mode.
std::string get_compilation_time() const
virtual void mark_failed()
Mark this component as failed.
bool is_failed() const
void status_set_warning(const char *message="unspecified")
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.
Definition component.cpp:72
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:87
value_type const & value() const
Definition optional.h:89
optional< RhtAccelerationMode > acceleration_mode_
Definition sen5x.h:126
sensor::Sensor * pm_4_0_sensor_
Definition sen5x.h:110
void dump_config() override
Definition sen5x.cpp:242
ESPPreferenceObject pref_
Definition sen5x.h:125
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning)
Definition sen5x.cpp:404
sensor::Sensor * pm_2_5_sensor_
Definition sen5x.h:109
sensor::Sensor * temperature_sensor_
Definition sen5x.h:113
optional< uint32_t > auto_cleaning_interval_
Definition sen5x.h:127
sensor::Sensor * pm_1_0_sensor_
Definition sen5x.h:108
sensor::Sensor * voc_sensor_
Definition sen5x.h:115
optional< TemperatureCompensation > temperature_compensation_
Definition sen5x.h:130
sensor::Sensor * nox_sensor_
Definition sen5x.h:117
optional< GasTuning > voc_tuning_params_
Definition sen5x.h:128
sensor::Sensor * pm_10_0_sensor_
Definition sen5x.h:111
Sen5xBaselines voc_baselines_storage_
Definition sen5x.h:122
sensor::Sensor * humidity_sensor_
Definition sen5x.h:114
bool write_temperature_compensation_(const TemperatureCompensation &compensation)
Definition sen5x.cpp:419
optional< GasTuning > nox_tuning_params_
Definition sen5x.h:129
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:39
const uint32_t SHORTEST_BASELINE_STORE_INTERVAL
Definition sen5x.h:23
RhtAccelerationMode
Definition sen5x.h:32
@ LOW_ACCELERATION
Definition sen5x.h:32
@ HIGH_ACCELERATION
Definition sen5x.h:32
@ MEDIUM_ACCELERATION
Definition sen5x.h:32
@ 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
const uint32_t MAXIMUM_STORAGE_DIFF
Definition sen5x.h:25
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
uint32_t fnv1_hash(const std::string &str)
Calculate a FNV-1 hash of str.
Definition helpers.cpp:186
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:200
ESPPreferences * global_preferences
void IRAM_ATTR HOT delay(uint32_t ms)
Definition core.cpp:28
Application App
Global storage of Application pointer - only one Application can exist.
uint16_t learning_time_gain_hours
Definition sen5x.h:37
uint16_t gating_max_duration_minutes
Definition sen5x.h:38
uint16_t learning_time_offset_hours
Definition sen5x.h:36
uint16_t temperature
Definition sun_gtil2.cpp:12