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