ESPHome 2026.5.0b1
Loading...
Searching...
No Matches
ens160_base.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// For best performance, the sensor shall be operated in normal indoor air in the range -5 to 60°C
9// (typical: 25°C); relative humidity: 20 to 80%RH (typical: 50%RH), non-condensing with no aggressive
10// or poisonous gases present. Prolonged exposure to environments outside these conditions can affect
11// performance and lifetime of the sensor.
12// The sensor is designed for indoor use and is not waterproof or dustproof. It should be protected from
13// water, condensation, dust, and aggressive gases. Note that the status will only be stored in non-volatile
14// memory after an initial 24 h of continuous operation. If unpowered before the conclusion of that period,
15// the ENS160 will resume "Initial Start-up" mode after re-powering.
16
17#include "ens160_base.h"
18#include "esphome/core/log.h"
19#include "esphome/core/hal.h"
20
22
23static const char *const TAG = "ens160";
24
25// Datasheet specifies 10ms, but some users report that 10ms is not sufficient for the
26// sensor to boot and be ready for commands. 11ms seems to be a safe value.
27static const uint8_t ENS160_BOOTING = 11;
28
29static const uint16_t ENS160_PART_ID = 0x0160;
30
31static const uint8_t ENS160_REG_PART_ID = 0x00;
32static const uint8_t ENS160_REG_OPMODE = 0x10;
33static const uint8_t ENS160_REG_CONFIG = 0x11;
34static const uint8_t ENS160_REG_COMMAND = 0x12;
35static const uint8_t ENS160_REG_TEMP_IN = 0x13;
36static const uint8_t ENS160_REG_DATA_STATUS = 0x20;
37static const uint8_t ENS160_REG_DATA_AQI = 0x21;
38static const uint8_t ENS160_REG_DATA_TVOC = 0x22;
39static const uint8_t ENS160_REG_DATA_ECO2 = 0x24;
40
41static const uint8_t ENS160_REG_GPR_READ_0 = 0x48;
42static const uint8_t ENS160_REG_GPR_READ_4 = ENS160_REG_GPR_READ_0 + 4;
43
44static const uint8_t ENS160_COMMAND_NOP = 0x00;
45static const uint8_t ENS160_COMMAND_CLRGPR = 0xCC;
46static const uint8_t ENS160_COMMAND_GET_APPVER = 0x0E;
47
48static const uint8_t ENS160_OPMODE_RESET = 0xF0;
49static const uint8_t ENS160_OPMODE_IDLE = 0x01;
50static const uint8_t ENS160_OPMODE_STD = 0x02;
51
52static const uint8_t ENS160_DATA_STATUS_STATAS = 0x80;
53static const uint8_t ENS160_DATA_STATUS_STATER = 0x40;
54static const uint8_t ENS160_DATA_STATUS_VALIDITY = 0x0C;
55static const uint8_t ENS160_DATA_STATUS_NEWDAT = 0x02;
56static const uint8_t ENS160_DATA_STATUS_NEWGPR = 0x01;
57
58// helps remove reserved bits in aqi data register
59static const uint8_t ENS160_DATA_AQI = 0x07;
60
62 // check part_id
63 uint16_t part_id;
64 if (!this->read_bytes(ENS160_REG_PART_ID, reinterpret_cast<uint8_t *>(&part_id), 2)) {
65 this->error_code_ = COMMUNICATION_FAILED;
66 this->mark_failed();
67 return;
68 }
69 if (part_id != ENS160_PART_ID) {
70 this->error_code_ = INVALID_ID;
71 this->mark_failed();
72 return;
73 }
74
75 // set mode to reset
76 if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_RESET)) {
77 this->error_code_ = WRITE_FAILED;
78 this->mark_failed();
79 return;
80 }
81 delay(ENS160_BOOTING);
82
83 // check status
84 uint8_t status_value;
85 if (!this->read_byte(ENS160_REG_DATA_STATUS, &status_value)) {
86 this->error_code_ = READ_FAILED;
87 this->mark_failed();
88 return;
89 }
90 this->validity_flag_ = static_cast<ValidityFlag>((ENS160_DATA_STATUS_VALIDITY & status_value) >> 2);
91
92 if (this->validity_flag_ == INVALID_OUTPUT) {
93 this->error_code_ = VALIDITY_INVALID;
94 this->mark_failed();
95 return;
96 }
97
98 // set mode to idle
99 if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_IDLE)) {
100 this->error_code_ = WRITE_FAILED;
101 this->mark_failed();
102 return;
103 }
104 delay(ENS160_BOOTING);
105
106 // clear command
107 if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_NOP)) {
108 this->error_code_ = WRITE_FAILED;
109 this->mark_failed();
110 return;
111 }
112 if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_CLRGPR)) {
113 this->error_code_ = WRITE_FAILED;
114 this->mark_failed();
115 return;
116 }
117 delay(ENS160_BOOTING);
118
119 // read firmware version
120 if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_GET_APPVER)) {
121 this->error_code_ = WRITE_FAILED;
122 this->mark_failed();
123 return;
124 }
125 delay(ENS160_BOOTING);
126
127 uint8_t version_data[3];
128 if (!this->read_bytes(ENS160_REG_GPR_READ_4, version_data, 3)) {
129 this->error_code_ = READ_FAILED;
130 this->mark_failed();
131 return;
132 }
133 this->firmware_ver_major_ = version_data[0];
134 this->firmware_ver_minor_ = version_data[1];
135 this->firmware_ver_build_ = version_data[2];
136
137 // set mode to standard
138 if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_STD)) {
139 this->error_code_ = WRITE_FAILED;
140 this->mark_failed();
141 return;
142 }
143
144 // read opmode and check standard mode is achieved before finishing Setup
145 uint8_t op_mode;
146 if (!this->read_byte(ENS160_REG_OPMODE, &op_mode)) {
147 this->error_code_ = READ_FAILED;
148 this->mark_failed();
149 return;
150 }
151
152 if (op_mode != ENS160_OPMODE_STD) {
153 this->error_code_ = STD_OPMODE_FAILED;
154 this->mark_failed();
155 return;
156 }
157}
158
160 uint8_t status_value, data_ready;
161
162 if (!this->read_byte(ENS160_REG_DATA_STATUS, &status_value)) {
163 ESP_LOGW(TAG, "Error reading status register");
164 this->status_set_warning();
165 return;
166 }
167
168 // verbose status logging
169 ESP_LOGV(TAG,
170 "ENS160 Status Register: 0x%02x\n"
171 " STATAS bit 0x%x\n"
172 " STATER bit 0x%x\n"
173 " VALIDITY FLAG 0x%02x\n"
174 " NEWDAT bit 0x%x\n"
175 " NEWGPR bit 0x%x",
176 status_value, (ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS,
177 (ENS160_DATA_STATUS_STATER & (status_value)) == ENS160_DATA_STATUS_STATER,
178 (ENS160_DATA_STATUS_VALIDITY & status_value) >> 2,
179 (ENS160_DATA_STATUS_NEWDAT & (status_value)) == ENS160_DATA_STATUS_NEWDAT,
180 (ENS160_DATA_STATUS_NEWGPR & (status_value)) == ENS160_DATA_STATUS_NEWGPR);
181
182 data_ready = ENS160_DATA_STATUS_NEWDAT & status_value;
183 this->validity_flag_ = static_cast<ValidityFlag>((ENS160_DATA_STATUS_VALIDITY & status_value) >> 2);
184
185 switch (validity_flag_) {
186 case NORMAL_OPERATION:
187 if (data_ready != ENS160_DATA_STATUS_NEWDAT) {
188 ESP_LOGD(TAG, "ENS160 readings unavailable - Normal Operation but readings not ready");
189 return;
190 }
191 break;
192 case INITIAL_STARTUP:
193 if (!this->initial_startup_) {
194 this->initial_startup_ = true;
195 ESP_LOGI(TAG, "ENS160 readings unavailable - 1 hour startup required after first power on");
196 }
197 return;
198 case WARMING_UP:
199 if (!this->warming_up_) {
200 this->warming_up_ = true;
201 ESP_LOGI(TAG, "ENS160 readings not available yet - Warming up requires 3 minutes");
202 this->send_env_data_();
203 }
204 return;
205 case INVALID_OUTPUT:
206 ESP_LOGE(TAG, "ENS160 Invalid Status - No valid output");
207 this->status_set_warning();
208 return;
209 }
210
211 // read new data
212 uint16_t data_eco2;
213 if (!this->read_bytes(ENS160_REG_DATA_ECO2, reinterpret_cast<uint8_t *>(&data_eco2), 2)) {
214 ESP_LOGW(TAG, "Error reading eCO2 data register");
215 this->status_set_warning();
216 return;
217 }
218 if (this->co2_ != nullptr) {
219 this->co2_->publish_state(data_eco2);
220 }
221
222 uint16_t data_tvoc;
223 if (!this->read_bytes(ENS160_REG_DATA_TVOC, reinterpret_cast<uint8_t *>(&data_tvoc), 2)) {
224 ESP_LOGW(TAG, "Error reading TVOC data register");
225 this->status_set_warning();
226 return;
227 }
228 if (this->tvoc_ != nullptr) {
229 this->tvoc_->publish_state(data_tvoc);
230 }
231
232 uint8_t data_aqi;
233 if (!this->read_byte(ENS160_REG_DATA_AQI, &data_aqi)) {
234 ESP_LOGW(TAG, "Error reading AQI data register");
235 this->status_set_warning();
236 return;
237 }
238 if (this->aqi_ != nullptr) {
239 // remove reserved bits, just in case they are used in future
240 data_aqi = ENS160_DATA_AQI & data_aqi;
241 this->aqi_->publish_state(data_aqi);
242 }
243
244 this->status_clear_warning();
245
246 // set temperature and humidity compensation data
247 this->send_env_data_();
248}
249
251 if (this->temperature_ == nullptr && this->humidity_ == nullptr)
252 return;
253
254 float temperature = NAN;
255 if (this->temperature_ != nullptr)
256 temperature = this->temperature_->state;
257
258 if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
259 ESP_LOGW(TAG, "Invalid external temperature - compensation values not updated");
260 return;
261 } else {
262 ESP_LOGV(TAG, "External temperature compensation: %.1f°C", temperature);
263 }
264
265 float humidity = NAN;
266 if (this->humidity_ != nullptr)
267 humidity = this->humidity_->state;
268
269 if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
270 ESP_LOGW(TAG, "Invalid external humidity - compensation values not updated");
271 return;
272 } else {
273 ESP_LOGV(TAG, "External humidity compensation: %.1f%%", humidity);
274 }
275
276 uint16_t t = (uint16_t) ((temperature + 273.15f) * 64.0f);
277 uint16_t h = (uint16_t) (humidity * 512.0f);
278
279 uint8_t data[4];
280 data[0] = t & 0xff;
281 data[1] = (t >> 8) & 0xff;
282 data[2] = h & 0xff;
283 data[3] = (h >> 8) & 0xff;
284
285 if (!this->write_bytes(ENS160_REG_TEMP_IN, data, 4)) {
286 ESP_LOGE(TAG, "Error writing compensation values");
287 this->status_set_warning();
288 return;
289 }
290}
291
293 ESP_LOGCONFIG(TAG, "ENS160:");
294
295 switch (this->error_code_) {
297 ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
298 break;
299 case READ_FAILED:
300 ESP_LOGE(TAG, "Error reading from register");
301 break;
302 case WRITE_FAILED:
303 ESP_LOGE(TAG, "Error writing to register");
304 break;
305 case INVALID_ID:
306 ESP_LOGE(TAG, "Sensor reported an invalid ID. Is this a ENS160?");
307 break;
308 case VALIDITY_INVALID:
309 ESP_LOGE(TAG, "Invalid Device Status - No valid output");
310 break;
312 ESP_LOGE(TAG, "Device failed to achieve Standard Operating Mode");
313 break;
314 case NONE:
315 ESP_LOGD(TAG, "Setup successful");
316 break;
317 }
318 ESP_LOGI(TAG, "Firmware Version: %d.%d.%d", this->firmware_ver_major_, this->firmware_ver_minor_,
319 this->firmware_ver_build_);
320
321 LOG_UPDATE_INTERVAL(this);
322 LOG_SENSOR(" ", "CO2 Sensor:", this->co2_);
323 LOG_SENSOR(" ", "TVOC Sensor:", this->tvoc_);
324 LOG_SENSOR(" ", "AQI Sensor:", this->aqi_);
325
326 if (this->temperature_ != nullptr && this->humidity_ != nullptr) {
327 LOG_SENSOR(" ", " Temperature Compensation:", this->temperature_);
328 LOG_SENSOR(" ", " Humidity Compensation:", this->humidity_);
329 } else {
330 ESP_LOGCONFIG(TAG, " Compensation: Not configured");
331 }
332}
333
334} // namespace esphome::ens160_base
uint8_t h
Definition bl0906.h:2
void mark_failed()
Mark this component as failed.
void status_clear_warning()
Definition component.h:306
virtual bool write_byte(uint8_t a_register, uint8_t data)=0
virtual bool read_byte(uint8_t a_register, uint8_t *data)=0
enum esphome::ens160_base::ENS160Component::ErrorCode NONE
virtual bool write_bytes(uint8_t a_register, uint8_t *data, size_t len)=0
enum esphome::ens160_base::ENS160Component::ValidityFlag validity_flag_
virtual bool read_bytes(uint8_t a_register, uint8_t *data, size_t len)=0
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:68
float state
This member variable stores the last state that has passed through all filters.
Definition sensor.h:138
void HOT delay(uint32_t ms)
Definition hal.cpp:82
uint16_t temperature
Definition sun_gtil2.cpp:12