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