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