ESPHome  2024.5.2
ens160.cpp
Go to the documentation of this file.
1 // ENS160 sensor with I2C interface from ScioSense
2 //
3 // Datasheet: https://www.sciosense.com/wp-content/uploads/documents/SC-001224-DS-7-ENS160-Datasheet.pdf
4 //
5 // Implementation based on:
6 // https://github.com/sciosense/ENS160_driver
7 
8 #include "ens160.h"
9 #include "esphome/core/log.h"
10 #include "esphome/core/hal.h"
11 
12 namespace esphome {
13 namespace ens160 {
14 
15 static const char *const TAG = "ens160";
16 
17 static const uint8_t ENS160_BOOTING = 10;
18 
19 static const uint16_t ENS160_PART_ID = 0x0160;
20 
21 static const uint8_t ENS160_REG_PART_ID = 0x00;
22 static const uint8_t ENS160_REG_OPMODE = 0x10;
23 static const uint8_t ENS160_REG_CONFIG = 0x11;
24 static const uint8_t ENS160_REG_COMMAND = 0x12;
25 static const uint8_t ENS160_REG_TEMP_IN = 0x13;
26 static const uint8_t ENS160_REG_DATA_STATUS = 0x20;
27 static const uint8_t ENS160_REG_DATA_AQI = 0x21;
28 static const uint8_t ENS160_REG_DATA_TVOC = 0x22;
29 static const uint8_t ENS160_REG_DATA_ECO2 = 0x24;
30 
31 static const uint8_t ENS160_REG_GPR_READ_0 = 0x48;
32 static const uint8_t ENS160_REG_GPR_READ_4 = ENS160_REG_GPR_READ_0 + 4;
33 
34 static const uint8_t ENS160_COMMAND_NOP = 0x00;
35 static const uint8_t ENS160_COMMAND_CLRGPR = 0xCC;
36 static const uint8_t ENS160_COMMAND_GET_APPVER = 0x0E;
37 
38 static const uint8_t ENS160_OPMODE_RESET = 0xF0;
39 static const uint8_t ENS160_OPMODE_IDLE = 0x01;
40 static const uint8_t ENS160_OPMODE_STD = 0x02;
41 
42 static const uint8_t ENS160_DATA_STATUS_STATAS = 0x80;
43 static const uint8_t ENS160_DATA_STATUS_STATER = 0x40;
44 static const uint8_t ENS160_DATA_STATUS_VALIDITY = 0x0C;
45 static const uint8_t ENS160_DATA_STATUS_NEWDAT = 0x02;
46 static const uint8_t ENS160_DATA_STATUS_NEWGPR = 0x01;
47 
48 // helps remove reserved bits in aqi data register
49 static const uint8_t ENS160_DATA_AQI = 0x07;
50 
52  ESP_LOGCONFIG(TAG, "Setting up ENS160...");
53 
54  // check part_id
55  uint16_t part_id;
56  if (!this->read_bytes(ENS160_REG_PART_ID, reinterpret_cast<uint8_t *>(&part_id), 2)) {
57  this->error_code_ = COMMUNICATION_FAILED;
58  this->mark_failed();
59  return;
60  }
61  if (part_id != ENS160_PART_ID) {
62  this->error_code_ = INVALID_ID;
63  this->mark_failed();
64  return;
65  }
66 
67  // set mode to reset
68  if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_RESET)) {
69  this->error_code_ = WRITE_FAILED;
70  this->mark_failed();
71  return;
72  }
73  delay(ENS160_BOOTING);
74 
75  // check status
76  uint8_t status_value;
77  if (!this->read_byte(ENS160_REG_DATA_STATUS, &status_value)) {
78  this->error_code_ = READ_FAILED;
79  this->mark_failed();
80  return;
81  }
82  this->validity_flag_ = static_cast<ValidityFlag>((ENS160_DATA_STATUS_VALIDITY & status_value) >> 2);
83 
84  if (this->validity_flag_ == INVALID_OUTPUT) {
85  this->error_code_ = VALIDITY_INVALID;
86  this->mark_failed();
87  return;
88  }
89 
90  // set mode to idle
91  if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_IDLE)) {
92  this->error_code_ = WRITE_FAILED;
93  this->mark_failed();
94  return;
95  }
96  // clear command
97  if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_NOP)) {
98  this->error_code_ = WRITE_FAILED;
99  this->mark_failed();
100  return;
101  }
102  if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_CLRGPR)) {
103  this->error_code_ = WRITE_FAILED;
104  this->mark_failed();
105  return;
106  }
107 
108  // read firmware version
109  if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_GET_APPVER)) {
110  this->error_code_ = WRITE_FAILED;
111  this->mark_failed();
112  return;
113  }
114  uint8_t version_data[3];
115  if (!this->read_bytes(ENS160_REG_GPR_READ_4, version_data, 3)) {
116  this->error_code_ = READ_FAILED;
117  this->mark_failed();
118  return;
119  }
120  this->firmware_ver_major_ = version_data[0];
121  this->firmware_ver_minor_ = version_data[1];
122  this->firmware_ver_build_ = version_data[2];
123 
124  // set mode to standard
125  if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_STD)) {
126  this->error_code_ = WRITE_FAILED;
127  this->mark_failed();
128  return;
129  }
130 
131  // read opmode and check standard mode is achieved before finishing Setup
132  uint8_t op_mode;
133  if (!this->read_byte(ENS160_REG_OPMODE, &op_mode)) {
134  this->error_code_ = READ_FAILED;
135  this->mark_failed();
136  return;
137  }
138 
139  if (op_mode != ENS160_OPMODE_STD) {
140  this->error_code_ = STD_OPMODE_FAILED;
141  this->mark_failed();
142  return;
143  }
144 }
145 
147  uint8_t status_value, data_ready;
148 
149  if (!this->read_byte(ENS160_REG_DATA_STATUS, &status_value)) {
150  ESP_LOGW(TAG, "Error reading status register");
151  this->status_set_warning();
152  return;
153  }
154 
155  // verbose status logging
156  ESP_LOGV(TAG, "Status: ENS160 STATAS bit 0x%x",
157  (ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS);
158  ESP_LOGV(TAG, "Status: ENS160 STATER bit 0x%x",
159  (ENS160_DATA_STATUS_STATER & (status_value)) == ENS160_DATA_STATUS_STATER);
160  ESP_LOGV(TAG, "Status: ENS160 VALIDITY FLAG 0x%02x", (ENS160_DATA_STATUS_VALIDITY & status_value) >> 2);
161  ESP_LOGV(TAG, "Status: ENS160 NEWDAT bit 0x%x",
162  (ENS160_DATA_STATUS_NEWDAT & (status_value)) == ENS160_DATA_STATUS_NEWDAT);
163  ESP_LOGV(TAG, "Status: ENS160 NEWGPR bit 0x%x",
164  (ENS160_DATA_STATUS_NEWGPR & (status_value)) == ENS160_DATA_STATUS_NEWGPR);
165 
166  data_ready = ENS160_DATA_STATUS_NEWDAT & status_value;
167  this->validity_flag_ = static_cast<ValidityFlag>((ENS160_DATA_STATUS_VALIDITY & status_value) >> 2);
168 
169  switch (validity_flag_) {
170  case NORMAL_OPERATION:
171  if (data_ready != ENS160_DATA_STATUS_NEWDAT) {
172  ESP_LOGD(TAG, "ENS160 readings unavailable - Normal Operation but readings not ready");
173  return;
174  }
175  break;
176  case INITIAL_STARTUP:
177  if (!this->initial_startup_) {
178  this->initial_startup_ = true;
179  ESP_LOGI(TAG, "ENS160 readings unavailable - 1 hour startup required after first power on");
180  }
181  return;
182  case WARMING_UP:
183  if (!this->warming_up_) {
184  this->warming_up_ = true;
185  ESP_LOGI(TAG, "ENS160 readings not available yet - Warming up requires 3 minutes");
186  this->send_env_data_();
187  }
188  return;
189  case INVALID_OUTPUT:
190  ESP_LOGE(TAG, "ENS160 Invalid Status - No Invalid Output");
191  this->status_set_warning();
192  return;
193  }
194 
195  // read new data
196  uint16_t data_eco2;
197  if (!this->read_bytes(ENS160_REG_DATA_ECO2, reinterpret_cast<uint8_t *>(&data_eco2), 2)) {
198  ESP_LOGW(TAG, "Error reading eCO2 data register");
199  this->status_set_warning();
200  return;
201  }
202  if (this->co2_ != nullptr) {
203  this->co2_->publish_state(data_eco2);
204  }
205 
206  uint16_t data_tvoc;
207  if (!this->read_bytes(ENS160_REG_DATA_TVOC, reinterpret_cast<uint8_t *>(&data_tvoc), 2)) {
208  ESP_LOGW(TAG, "Error reading TVOC data register");
209  this->status_set_warning();
210  return;
211  }
212  if (this->tvoc_ != nullptr) {
213  this->tvoc_->publish_state(data_tvoc);
214  }
215 
216  uint8_t data_aqi;
217  if (!this->read_byte(ENS160_REG_DATA_AQI, &data_aqi)) {
218  ESP_LOGW(TAG, "Error reading AQI data register");
219  this->status_set_warning();
220  return;
221  }
222  if (this->aqi_ != nullptr) {
223  // remove reserved bits, just in case they are used in future
224  data_aqi = ENS160_DATA_AQI & data_aqi;
225 
226  this->aqi_->publish_state(data_aqi);
227  }
228 
229  this->status_clear_warning();
230 
231  // set temperature and humidity compensation data
232  this->send_env_data_();
233 }
234 
236  if (this->temperature_ == nullptr && this->humidity_ == nullptr)
237  return;
238 
239  float temperature = NAN;
240  if (this->temperature_ != nullptr)
241  temperature = this->temperature_->state;
242 
243  if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
244  ESP_LOGW(TAG, "Invalid external temperature - compensation values not updated");
245  return;
246  } else {
247  ESP_LOGV(TAG, "External temperature compensation: %.1f°C", temperature);
248  }
249 
250  float humidity = NAN;
251  if (this->humidity_ != nullptr)
252  humidity = this->humidity_->state;
253 
254  if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
255  ESP_LOGW(TAG, "Invalid external humidity - compensation values not updated");
256  return;
257  } else {
258  ESP_LOGV(TAG, "External humidity compensation: %.1f%%", humidity);
259  }
260 
261  uint16_t t = (uint16_t) ((temperature + 273.15f) * 64.0f);
262  uint16_t h = (uint16_t) (humidity * 512.0f);
263 
264  uint8_t data[4];
265  data[0] = t & 0xff;
266  data[1] = (t >> 8) & 0xff;
267  data[2] = h & 0xff;
268  data[3] = (h >> 8) & 0xff;
269 
270  if (!this->write_bytes(ENS160_REG_TEMP_IN, data, 4)) {
271  ESP_LOGE(TAG, "Error writing compensation values");
272  this->status_set_warning();
273  return;
274  }
275 }
276 
278  ESP_LOGCONFIG(TAG, "ENS160:");
279 
280  switch (this->error_code_) {
282  ESP_LOGE(TAG, "Communication failed! Is the sensor connected?");
283  break;
284  case READ_FAILED:
285  ESP_LOGE(TAG, "Error reading from register");
286  break;
287  case WRITE_FAILED:
288  ESP_LOGE(TAG, "Error writing to register");
289  break;
290  case INVALID_ID:
291  ESP_LOGE(TAG, "Sensor reported an invalid ID. Is this a ENS160?");
292  break;
293  case VALIDITY_INVALID:
294  ESP_LOGE(TAG, "Invalid Device Status - No valid output");
295  break;
296  case STD_OPMODE_FAILED:
297  ESP_LOGE(TAG, "Device failed to achieve Standard Operating Mode");
298  break;
299  case NONE:
300  ESP_LOGD(TAG, "Setup successful");
301  break;
302  }
303  ESP_LOGI(TAG, "Firmware Version: %d.%d.%d", this->firmware_ver_major_, this->firmware_ver_minor_,
304  this->firmware_ver_build_);
305 
306  LOG_I2C_DEVICE(this);
307  LOG_UPDATE_INTERVAL(this);
308  LOG_SENSOR(" ", "CO2 Sensor:", this->co2_);
309  LOG_SENSOR(" ", "TVOC Sensor:", this->tvoc_);
310  LOG_SENSOR(" ", "AQI Sensor:", this->aqi_);
311 
312  if (this->temperature_ != nullptr && this->humidity_ != nullptr) {
313  LOG_SENSOR(" ", " Temperature Compensation:", this->temperature_);
314  LOG_SENSOR(" ", " Humidity Compensation:", this->humidity_);
315  } else {
316  ESP_LOGCONFIG(TAG, " Compensation: Not configured");
317  }
318 }
319 
320 } // namespace ens160
321 } // namespace esphome
bool read_byte(uint8_t a_register, uint8_t *data, bool stop=true)
Definition: i2c.h:235
sensor::Sensor * humidity_
Definition: ens160.h:55
void status_set_warning(const char *message="unspecified")
Definition: component.cpp:151
sensor::Sensor * aqi_
Definition: ens160.h:53
bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len)
Compat APIs All methods below have been added for compatibility reasons.
Definition: i2c.h:212
sensor::Sensor * tvoc_
Definition: ens160.h:52
float state
This member variable stores the last state that has passed through all filters.
Definition: sensor.h:131
void status_clear_warning()
Definition: component.cpp:166
void publish_state(float state)
Publish a new state to the front-end.
Definition: sensor.cpp:39
uint16_t temperature
Definition: sun_gtil2.cpp:26
sensor::Sensor * co2_
Definition: ens160.h:51
bool write_byte(uint8_t a_register, uint8_t data, bool stop=true)
Definition: i2c.h:262
virtual void mark_failed()
Mark this component as failed.
Definition: component.cpp:118
uint8_t h
Definition: bl0939.h:21
This is a workaround until we can figure out a way to get the tflite-micro idf component code availab...
Definition: a01nyub.cpp:7
sensor::Sensor * temperature_
Definition: ens160.h:56
enum esphome::ens160::ENS160Component::ErrorCode NONE
void IRAM_ATTR HOT delay(uint32_t ms)
Definition: core.cpp:26
enum esphome::ens160::ENS160Component::ValidityFlag validity_flag_
bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len, bool stop=true)
Definition: i2c.h:248