ESPHome 2026.11.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::sen5x {
9
10ESPHOME_LOG_TAG(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
32static const LogString *type_to_string(Sen5xType type) {
33 switch (type) {
35 return LOG_STR("SEN50");
37 return LOG_STR("SEN54");
39 return LOG_STR("SEN55");
40 default:
41 return LOG_STR("UNKNOWN");
42 }
43}
44
45static const LogString *rht_accel_mode_to_string(RhtAccelerationMode mode) {
46 switch (mode) {
48 return LOG_STR("LOW");
50 return LOG_STR("MEDIUM");
52 return LOG_STR("HIGH");
53 default:
54 return LOG_STR("UNKNOWN");
55 }
56}
57
59 // the sensor needs 1000 ms to enter the idle state
60 this->set_timeout(1000, [this]() {
61 // Check if measurement is ready before reading the value
62 if (!this->write_command(SEN5X_CMD_GET_DATA_READY_STATUS)) {
63 this->mark_failed(LOG_STR("Failed to write data ready status command"));
64 return;
65 }
66 delay(20); // per datasheet
67
68 uint16_t raw_read_status;
69 if (!this->read_data(raw_read_status)) {
70 this->mark_failed(LOG_STR("Failed to read data ready status"));
71 return;
72 }
73
74 uint32_t stop_measurement_delay = 0;
75 // In order to query the device periodic measurement must be ceased
76 if (raw_read_status) {
77 ESP_LOGD(TAG, "Data is available; stopping periodic measurement");
78 if (!this->write_command(SEN5X_CMD_STOP_MEASUREMENTS)) {
79 this->mark_failed(LOG_STR("Failed to stop measurements"));
80 return;
81 }
82 // According to the SEN5x datasheet the sensor will only respond to other commands after waiting 200 ms after
83 // issuing the stop_periodic_measurement command
84 stop_measurement_delay = 200;
85 }
86 this->set_timeout(stop_measurement_delay, [this]() {
87 // note: serial number register is actually 32-bytes long but we grab only the first 16-bytes,
88 // this appears to be all that Sensirion uses for serial numbers, this could change
89 uint16_t raw_serial_number[8];
90 if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 8, 20)) {
91 this->mark_failed(LOG_STR("Unable to read serial ID"));
92 return;
93 }
94 const char *serial_number = sensirion_convert_to_string_in_place(raw_serial_number, 8);
95 snprintf(this->serial_number_, sizeof(this->serial_number_), "%s", serial_number);
96 ESP_LOGV(TAG, "Serial number %s", this->serial_number_);
97
98 uint16_t raw_product_name[16];
99 Sen5xType detected_type = Sen5xType::UNKNOWN;
100 if (this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) {
101 const char *product_name = sensirion_convert_to_string_in_place(raw_product_name, 16);
102 if (strncmp(product_name, "SEN50", 5) == 0) {
103 detected_type = Sen5xType::SEN50;
104 } else if (strncmp(product_name, "SEN54", 5) == 0) {
105 detected_type = Sen5xType::SEN54;
106 } else if (strncmp(product_name, "SEN55", 5) == 0) {
107 detected_type = Sen5xType::SEN55;
108 }
109 }
110
111 if (this->model_override_.has_value()) {
112 if (detected_type != this->model_override_.value()) {
113 ESP_LOGW(TAG, "Detected %s, using %s", LOG_STR_ARG(type_to_string(detected_type)),
114 LOG_STR_ARG(type_to_string(this->model_override_.value())));
115 }
116 this->type_ = this->model_override_.value();
117 } else if (detected_type == Sen5xType::UNKNOWN) {
119 this->mark_failed(LOG_STR("Unable to read product name"));
120 return;
121 } else {
122 this->type_ = detected_type;
123 }
124
125 ESP_LOGD(TAG, "Type: %s", LOG_STR_ARG(type_to_string(this->type_)));
126 if (this->humidity_sensor_ && this->type_ == Sen5xType::SEN50) {
127 ESP_LOGE(TAG, "Relative humidity requires a SEN54 or SEN55");
128 this->humidity_sensor_ = nullptr; // mark as not used
129 }
130 if (this->temperature_sensor_ && this->type_ == Sen5xType::SEN50) {
131 ESP_LOGE(TAG, "Temperature requires a SEN54 or SEN55");
132 this->temperature_sensor_ = nullptr; // mark as not used
133 }
134 if (this->voc_sensor_ && this->type_ == Sen5xType::SEN50) {
135 ESP_LOGE(TAG, "VOC requires a SEN54 or SEN55");
136 this->voc_sensor_ = nullptr; // mark as not used
137 }
138 if (this->nox_sensor_ && this->type_ != Sen5xType::SEN55) {
139 ESP_LOGE(TAG, "NOx requires a SEN55");
140 this->nox_sensor_ = nullptr; // mark as not used
141 }
142
143 if (!this->get_register(SEN5X_CMD_GET_FIRMWARE_VERSION, this->firmware_version_, 20)) {
144 this->mark_failed(LOG_STR("Unable to read firmware version"));
145 return;
146 }
147 this->firmware_version_ >>= 8;
148 ESP_LOGV(TAG, "Firmware version %d", this->firmware_version_);
149
150 if (this->voc_sensor_ && this->store_baseline_) {
151 // Hash with serial number, serial numbers are unique, so multiple sensors can be used without conflict
152 uint32_t hash = fnv1a_hash(this->serial_number_);
153 this->pref_ = global_preferences->make_preference<uint16_t[4]>(hash, true);
155 if (this->pref_.load(&this->voc_baseline_state_)) {
156 if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, this->voc_baseline_state_, 4)) {
157 ESP_LOGE(TAG, "VOC Baseline State write to sensor failed");
158 } else {
159 ESP_LOGV(TAG, "VOC Baseline State loaded");
160 delay(20);
161 }
162 }
163 }
164 bool result;
165 if (this->auto_cleaning_interval_.has_value()) {
166 // override default value
167 result = this->write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL, this->auto_cleaning_interval_.value());
168 } else {
169 result = this->write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL);
170 }
171 if (result) {
172 delay(20);
173 uint16_t secs[2];
174 if (this->read_data(secs, 2)) {
175 this->auto_cleaning_interval_ = secs[0] << 16 | secs[1];
176 }
177 }
178 if (this->acceleration_mode_.has_value()) {
179 result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE, this->acceleration_mode_.value());
180 } else {
181 result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE);
182 }
183 if (!result) {
184 this->mark_failed(LOG_STR("Failed to set rh/t acceleration mode"));
185 return;
186 }
187 delay(20);
188 if (!this->acceleration_mode_.has_value()) {
189 uint16_t mode;
190 if (this->read_data(mode)) {
192 } else {
193 ESP_LOGE(TAG, "Failed to read RHT Acceleration mode");
194 }
195 }
196 if (this->voc_tuning_params_.has_value()) {
197 this->write_tuning_parameters_(SEN5X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value());
198 delay(20);
199 }
200 if (this->nox_tuning_params_.has_value()) {
201 this->write_tuning_parameters_(SEN5X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value());
202 delay(20);
203 }
204
205 if (this->temperature_compensation_.has_value()) {
207 delay(20);
208 }
209
210 // Finally start sensor measurements
211 auto cmd = SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY;
212 if (this->pm_1_0_sensor_ || this->pm_2_5_sensor_ || this->pm_4_0_sensor_ || this->pm_10_0_sensor_) {
213 // if any of the gas sensors are active we need a full measurement
214 cmd = SEN5X_CMD_START_MEASUREMENTS;
215 }
216
217 if (!this->write_command(cmd)) {
218 this->mark_failed(LOG_STR("Error starting continuous measurements"));
219 return;
220 }
221 this->initialized_ = true;
222 });
223 });
224}
225
227 ESP_LOGCONFIG(TAG, "SEN5X:");
228 LOG_I2C_DEVICE(this);
229 ESP_LOGCONFIG(TAG,
230 " Type: %s%s\n"
231 " Firmware version: %d\n"
232 " Serial number: %s",
233 LOG_STR_ARG(type_to_string(this->type_)),
234 this->model_override_.has_value() ? LOG_STR_LITERAL(" (overridden)") : LOG_STR_LITERAL(""),
235 this->firmware_version_, this->serial_number_);
236 if (this->auto_cleaning_interval_.has_value()) {
237 ESP_LOGCONFIG(TAG, " Auto cleaning interval: %" PRId32 "s", this->auto_cleaning_interval_.value());
238 }
239 if (this->acceleration_mode_.has_value()) {
240 ESP_LOGCONFIG(TAG, " RH/T acceleration mode: %s",
241 LOG_STR_ARG(rht_accel_mode_to_string(this->acceleration_mode_.value())));
242 }
243 if (this->voc_sensor_) {
244 char hex_buf[5 * 4];
245 format_hex_pretty_to(hex_buf, this->voc_baseline_state_, 4, 0);
246 ESP_LOGCONFIG(TAG,
247 " Store Baseline: %s\n"
248 " State: %s\n",
249 TRUEFALSE(this->store_baseline_), hex_buf);
250 }
251 LOG_UPDATE_INTERVAL(this);
252 LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_);
253 LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_);
254 LOG_SENSOR(" ", "PM 4.0", this->pm_4_0_sensor_);
255 LOG_SENSOR(" ", "PM 10.0", this->pm_10_0_sensor_);
256 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
257 LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
258 LOG_SENSOR(" ", "VOC", this->voc_sensor_); // SEN54 and SEN55 only
259 LOG_SENSOR(" ", "NOx", this->nox_sensor_); // SEN55 only
260}
261
263 if (!this->initialized_) {
264 return;
265 }
266
267 if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) {
268 this->status_set_warning();
269 ESP_LOGD(TAG, "Write error: read measurement (%d)", this->last_error_);
270 return;
271 }
272 this->set_timeout(20, [this]() {
273 uint16_t measurements[8];
274
275 if (!this->read_data(measurements, 8)) {
276 this->status_set_warning();
277 ESP_LOGD(TAG, "Read data error (%d)", this->last_error_);
278 return;
279 }
280
281 ESP_LOGVV(TAG, "pm_1_0 = 0x%.4x", measurements[0]);
282 float pm_1_0 = measurements[0] == UINT16_MAX ? NAN : measurements[0] / 10.0f;
283
284 ESP_LOGVV(TAG, "pm_2_5 = 0x%.4x", measurements[1]);
285 float pm_2_5 = measurements[1] == UINT16_MAX ? NAN : measurements[1] / 10.0f;
286
287 ESP_LOGVV(TAG, "pm_4_0 = 0x%.4x", measurements[2]);
288 float pm_4_0 = measurements[2] == UINT16_MAX ? NAN : measurements[2] / 10.0f;
289
290 ESP_LOGVV(TAG, "pm_10_0 = 0x%.4x", measurements[3]);
291 float pm_10_0 = measurements[3] == UINT16_MAX ? NAN : measurements[3] / 10.0f;
292
293 ESP_LOGVV(TAG, "humidity = 0x%.4x", measurements[4]);
294 float humidity = measurements[4] == INT16_MAX ? NAN : static_cast<int16_t>(measurements[4]) / 100.0f;
295
296 ESP_LOGVV(TAG, "temperature = 0x%.4x", measurements[5]);
297 float temperature = measurements[5] == INT16_MAX ? NAN : static_cast<int16_t>(measurements[5]) / 200.0f;
298
299 ESP_LOGVV(TAG, "voc = 0x%.4x", measurements[6]);
300 int16_t voc_idx = static_cast<int16_t>(measurements[6]);
301 float voc = (voc_idx < SEN5X_MIN_INDEX_VALUE || voc_idx > SEN5X_MAX_INDEX_VALUE)
302 ? NAN
303 : static_cast<float>(voc_idx) / 10.0f;
304
305 ESP_LOGVV(TAG, "nox = 0x%.4x", measurements[7]);
306 int16_t nox_idx = static_cast<int16_t>(measurements[7]);
307 float nox = (nox_idx < SEN5X_MIN_INDEX_VALUE || nox_idx > SEN5X_MAX_INDEX_VALUE)
308 ? NAN
309 : static_cast<float>(nox_idx) / 10.0f;
310
311 if (this->pm_1_0_sensor_ != nullptr) {
312 this->pm_1_0_sensor_->publish_state(pm_1_0);
313 }
314 if (this->pm_2_5_sensor_ != nullptr) {
315 this->pm_2_5_sensor_->publish_state(pm_2_5);
316 }
317 if (this->pm_4_0_sensor_ != nullptr) {
318 this->pm_4_0_sensor_->publish_state(pm_4_0);
319 }
320 if (this->pm_10_0_sensor_ != nullptr) {
321 this->pm_10_0_sensor_->publish_state(pm_10_0);
322 }
323 if (this->temperature_sensor_ != nullptr) {
324 this->temperature_sensor_->publish_state(temperature);
325 }
326 if (this->humidity_sensor_ != nullptr) {
327 this->humidity_sensor_->publish_state(humidity);
328 }
329 if (this->voc_sensor_ != nullptr) {
330 this->voc_sensor_->publish_state(voc);
331 }
332 if (this->nox_sensor_ != nullptr) {
333 this->nox_sensor_->publish_state(nox);
334 }
335
336 if (!this->voc_sensor_ || !this->store_baseline_ ||
337 (App.get_loop_component_start_time() - this->voc_baseline_time_) < SHORTEST_BASELINE_STORE_INTERVAL) {
338 this->status_clear_warning();
339 } else {
341 if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) {
342 this->status_set_warning();
343 ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
344 } else {
345 this->set_timeout(20, [this]() {
346 if (!this->read_data(this->voc_baseline_state_, 4)) {
347 this->status_set_warning();
348 ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
349 } else {
350 if (this->pref_.save(&this->voc_baseline_state_)) {
351 ESP_LOGD(TAG, "VOC Baseline State saved");
352 }
353 this->status_clear_warning();
354 }
355 });
356 }
357 }
358 });
359}
360
361bool SEN5XComponent::write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning) {
362 uint16_t params[6];
363 params[0] = tuning.index_offset;
364 params[1] = tuning.learning_time_offset_hours;
365 params[2] = tuning.learning_time_gain_hours;
366 params[3] = tuning.gating_max_duration_minutes;
367 params[4] = tuning.std_initial;
368 params[5] = tuning.gain_factor;
369 auto result = write_command(i2c_command, params, 6);
370 if (!result) {
371 ESP_LOGE(TAG, "Set tuning parameters failed (command=%0xX, err=%d)", i2c_command, this->last_error_);
372 }
373 return result;
374}
375
377 uint16_t params[3];
378 params[0] = compensation.offset;
379 params[1] = compensation.normalized_offset_slope;
380 params[2] = compensation.time_constant;
381 if (!write_command(SEN5X_CMD_TEMPERATURE_COMPENSATION, params, 3)) {
382 ESP_LOGE(TAG, "Set temperature_compensation failed (%d)", this->last_error_);
383 return false;
384 }
385 return true;
386}
387
389 if (!write_command(SEN5X_CMD_START_CLEANING_FAN)) {
390 this->status_set_warning();
391 ESP_LOGE(TAG, "Start fan cleaning failed (%d)", this->last_error_);
392 return false;
393 } else {
394 ESP_LOGD(TAG, "Fan auto clean started");
395 }
396 return true;
397}
398
399} // namespace esphome::sen5x
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.
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
Definition component.cpp:96
void status_clear_warning()
Definition component.h:296
optional< RhtAccelerationMode > acceleration_mode_
Definition sen5x.h:115
sensor::Sensor * pm_4_0_sensor_
Definition sen5x.h:106
void dump_config() override
Definition sen5x.cpp:226
ESPPreferenceObject pref_
Definition sen5x.h:121
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning)
Definition sen5x.cpp:361
optional< Sen5xType > model_override_
Definition sen5x.h:120
sensor::Sensor * pm_2_5_sensor_
Definition sen5x.h:105
sensor::Sensor * temperature_sensor_
Definition sen5x.h:109
optional< uint32_t > auto_cleaning_interval_
Definition sen5x.h:116
sensor::Sensor * pm_1_0_sensor_
Definition sen5x.h:104
sensor::Sensor * voc_sensor_
Definition sen5x.h:111
optional< TemperatureCompensation > temperature_compensation_
Definition sen5x.h:119
sensor::Sensor * nox_sensor_
Definition sen5x.h:113
optional< GasTuning > voc_tuning_params_
Definition sen5x.h:117
sensor::Sensor * pm_10_0_sensor_
Definition sen5x.h:107
sensor::Sensor * humidity_sensor_
Definition sen5x.h:110
bool write_temperature_compensation_(const TemperatureCompensation &compensation)
Definition sen5x.cpp:376
optional< GasTuning > nox_tuning_params_
Definition sen5x.h:118
uint16_t voc_baseline_state_[4]
Definition sen5x.h:97
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
uint8_t type
ESPHOME_LOG_TAG(TAG, "sen5x")
RhtAccelerationMode
Definition sen5x.h:11
@ LOW_ACCELERATION
Definition sen5x.h:12
@ HIGH_ACCELERATION
Definition sen5x.h:14
@ MEDIUM_ACCELERATION
Definition sen5x.h:13
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:498
ESPPreferences * global_preferences
void HOT delay(uint32_t ms)
Definition hal.cpp:85
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:973
static void uint32_t
ESPPreferenceObject make_preference(size_t, uint32_t, bool)
Definition preferences.h:24
uint16_t learning_time_gain_hours
Definition sen5x.h:22
uint16_t gating_max_duration_minutes
Definition sen5x.h:23
uint16_t learning_time_offset_hours
Definition sen5x.h:21
uint16_t temperature
Definition sun_gtil2.cpp:12
char serial_number[10]
Definition sun_gtil2.cpp:15