ESPHome 2025.12.0-dev
Loading...
Searching...
No Matches
veml7700.cpp
Go to the documentation of this file.
1#include "veml7700.h"
3#include "esphome/core/log.h"
4#include <limits>
5
6namespace esphome {
7namespace veml7700 {
8
9static const char *const TAG = "veml7700";
10static const size_t VEML_REG_SIZE = 2;
11
12static float reduce_to_zero(float a, float b) { return (a > b) ? (a - b) : 0; }
13
14template<typename T, size_t size> T get_next(const T (&array)[size], const T val) {
15 size_t i = 0;
16 size_t idx = std::numeric_limits<size_t>::max();
17 while (idx == std::numeric_limits<size_t>::max() && i < size) {
18 if (array[i] == val) {
19 idx = i;
20 break;
21 }
22 i++;
23 }
24 if (idx == std::numeric_limits<size_t>::max() || i + 1 >= size)
25 return val;
26 return array[i + 1];
27}
28
29template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) {
30 size_t i = size - 1;
31 size_t idx = std::numeric_limits<size_t>::max();
32 while (idx == std::numeric_limits<size_t>::max() && i > 0) {
33 if (array[i] == val) {
34 idx = i;
35 break;
36 }
37 i--;
38 }
39 if (idx == std::numeric_limits<size_t>::max() || i == 0)
40 return val;
41 return array[i - 1];
42}
43
44static uint16_t get_itime_ms(IntegrationTime time) {
45 uint16_t ms = 0;
46 switch (time) {
48 ms = 100;
49 break;
51 ms = 200;
52 break;
54 ms = 400;
55 break;
57 ms = 800;
58 break;
60 ms = 50;
61 break;
63 ms = 25;
64 break;
65 default:
66 ms = 100;
67 }
68 return ms;
69}
70
71static float get_gain_coeff(Gain gain) {
72 static const float GAIN_FLOAT[GAINS_COUNT] = {1.0f, 2.0f, 0.125f, 0.25f};
73 return GAIN_FLOAT[gain & 0b11];
74}
75
76static const char *get_gain_str(Gain gain) {
77 static const char *gain_str[GAINS_COUNT] = {"1x", "2x", "1/8x", "1/4x"};
78 return gain_str[gain & 0b11];
79}
80
82 auto err = this->configure_();
83 if (err != i2c::ERROR_OK) {
84 ESP_LOGW(TAG, "Sensor configuration failed");
85 this->mark_failed();
86 } else {
87 this->state_ = State::INITIAL_SETUP_COMPLETED;
88 }
89}
90
92 LOG_I2C_DEVICE(this);
93 ESP_LOGCONFIG(TAG, " Automatic gain/time: %s", YESNO(this->automatic_mode_enabled_));
94 if (!this->automatic_mode_enabled_) {
95 ESP_LOGCONFIG(TAG,
96 " Gain: %s\n"
97 " Integration time: %d ms",
98 get_gain_str(this->gain_), get_itime_ms(this->integration_time_));
99 }
100 ESP_LOGCONFIG(TAG,
101 " Lux compensation: %s\n"
102 " Glass attenuation factor: %f",
104 LOG_UPDATE_INTERVAL(this);
105
106 LOG_SENSOR(" ", "ALS channel lux", this->ambient_light_sensor_);
107 LOG_SENSOR(" ", "ALS channel counts", this->ambient_light_counts_sensor_);
108 LOG_SENSOR(" ", "WHITE channel lux", this->white_sensor_);
109 LOG_SENSOR(" ", "WHITE channel counts", this->white_counts_sensor_);
110 LOG_SENSOR(" ", "FAKE_IR channel lux", this->fake_infrared_sensor_);
111 LOG_SENSOR(" ", "Actual gain", this->actual_gain_sensor_);
112 LOG_SENSOR(" ", "Actual integration time", this->actual_integration_time_sensor_);
113
114 if (this->is_failed()) {
115 ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
116 }
117}
118
120 if (this->is_ready() && this->state_ == State::IDLE) {
121 ESP_LOGV(TAG, "Update: Initiating new data collection");
122
124
125 this->readings_.als_counts = 0;
126 this->readings_.white_counts = 0;
128 this->readings_.actual_gain = this->gain_;
129 this->readings_.als_lux = 0;
130 this->readings_.white_lux = 0;
132 } else {
133 ESP_LOGV(TAG, "Update: Component not ready yet");
134 }
135}
136
139
140 if (this->state_ == State::INITIAL_SETUP_COMPLETED) {
141 // Datasheet: 2.5 ms before the first measurement is needed, allowing for the correct start of the signal processor
142 // and oscillator.
143 // Reality: wait for couple integration times to have first samples captured
144 this->set_timeout(2 * this->integration_time_, [this]() { this->state_ = State::IDLE; });
145 }
146
147 if (this->is_ready()) {
148 switch (this->state_) {
149 case State::IDLE:
150 // doing nothing, having best time
151 break;
152
154 err = this->read_sensor_output_(this->readings_);
155 this->state_ = (err == i2c::ERROR_OK) ? State::DATA_COLLECTED : State::IDLE;
156 break;
157
158 case State::COLLECTING_DATA_AUTO: // Automatic mode - we start here to reconfigure device first
160 if (!this->are_adjustments_required_(this->readings_)) {
161 this->state_ = State::READY_TO_PUBLISH_PART_1;
162 } else {
163 // if sensitivity adjustment needed -
164 // shutdown device to change config and wait one integration time period
165 this->state_ = State::ADJUSTMENT_IN_PROGRESS;
166 err = this->reconfigure_time_and_gain_(this->readings_.actual_time, this->readings_.actual_gain, true);
167 if (err == i2c::ERROR_OK) {
168 this->set_timeout(1 * get_itime_ms(this->readings_.actual_time),
169 [this]() { this->state_ = State::READY_TO_APPLY_ADJUSTMENTS; });
170 } else {
171 this->state_ = State::IDLE;
172 }
173 }
174 break;
175
177 // nothing to be done, just waiting for the timeout
178 break;
179
181 // second stage of sensitivity adjustment - turn device back on
182 // and wait 2-3 integration time periods to get good data samples
183 this->state_ = State::ADJUSTMENT_IN_PROGRESS;
184 err = this->reconfigure_time_and_gain_(this->readings_.actual_time, this->readings_.actual_gain, false);
185 if (err == i2c::ERROR_OK) {
186 this->set_timeout(3 * get_itime_ms(this->readings_.actual_time),
187 [this]() { this->state_ = State::COLLECTING_DATA; });
188 } else {
189 this->state_ = State::IDLE;
190 }
191 break;
192
194 this->status_clear_warning();
195
199
200 this->publish_data_part_1_(this->readings_);
201 this->state_ = State::READY_TO_PUBLISH_PART_2;
202 break;
203
205 this->publish_data_part_2_(this->readings_);
206 this->state_ = State::READY_TO_PUBLISH_PART_3;
207 break;
208
210 this->publish_data_part_3_(this->readings_);
211 this->state_ = State::IDLE;
212 break;
213
214 default:
215 break;
216 }
217 if (err != i2c::ERROR_OK)
218 this->status_set_warning();
219 }
220}
221
223 ESP_LOGV(TAG, "Configure");
224
225 ConfigurationRegister als_conf{0};
226 als_conf.ALS_INT_EN = false;
227 als_conf.ALS_PERS = Persistence::PERSISTENCE_1;
228 als_conf.ALS_IT = this->integration_time_;
229 als_conf.ALS_GAIN = this->gain_;
230
231 als_conf.ALS_SD = true;
232 ESP_LOGV(TAG, "Shutdown before config. ALS_CONF_0 to 0x%04X", als_conf.raw);
233 auto err = this->write_register((uint8_t) CommandRegisters::ALS_CONF_0, als_conf.raw_bytes, VEML_REG_SIZE);
234 if (err != i2c::ERROR_OK) {
235 ESP_LOGW(TAG, "Failed to shutdown, I2C error %d", err);
236 return err;
237 }
238 delay(3);
239
240 als_conf.ALS_SD = false;
241 ESP_LOGV(TAG, "Turning on. Setting ALS_CONF_0 to 0x%04X", als_conf.raw);
242 err = this->write_register((uint8_t) CommandRegisters::ALS_CONF_0, als_conf.raw_bytes, VEML_REG_SIZE);
243 if (err != i2c::ERROR_OK) {
244 ESP_LOGW(TAG, "Failed to turn on, I2C error %d", err);
245 return err;
246 }
247
248 PSMRegister psm{0};
250 psm.PSM_EN = false;
251 ESP_LOGV(TAG, "Setting PSM to 0x%04X", psm.raw);
252 err = this->write_register((uint8_t) CommandRegisters::PWR_SAVING, psm.raw_bytes, VEML_REG_SIZE);
253 if (err != i2c::ERROR_OK) {
254 ESP_LOGW(TAG, "Failed to set PSM, I2C error %d", err);
255 return err;
256 }
257
258 return err;
259}
260
262 ESP_LOGV(TAG, "Reconfigure time and gain (%d ms, %s) %s", get_itime_ms(time), get_gain_str(gain),
263 shutdown ? "Shutting down" : "Turning back on");
264
265 ConfigurationRegister als_conf{0};
266 als_conf.raw = 0;
267
268 // We have to before changing parameters
269 als_conf.ALS_SD = shutdown;
270 als_conf.ALS_INT_EN = false;
271 als_conf.ALS_PERS = Persistence::PERSISTENCE_1;
272 als_conf.ALS_IT = time;
273 als_conf.ALS_GAIN = gain;
274 auto err = this->write_register((uint8_t) CommandRegisters::ALS_CONF_0, als_conf.raw_bytes, VEML_REG_SIZE);
275 if (err != i2c::ERROR_OK) {
276 ESP_LOGW(TAG, "%s failed", shutdown ? "Shutdown" : "Turn on");
277 }
278
279 return err;
280}
281
283 auto als_err = this->read_register((uint8_t) CommandRegisters::ALS, (uint8_t *) &data.als_counts, VEML_REG_SIZE);
284 if (als_err != i2c::ERROR_OK) {
285 ESP_LOGW(TAG, "Error reading ALS register, err = %d", als_err);
286 }
287 auto white_err =
288 this->read_register((uint8_t) CommandRegisters::WHITE, (uint8_t *) &data.white_counts, VEML_REG_SIZE);
289 if (white_err != i2c::ERROR_OK) {
290 ESP_LOGW(TAG, "Error reading WHITE register, err = %d", white_err);
291 }
292
293 ConfigurationRegister conf{0};
294 auto err = this->read_register((uint8_t) CommandRegisters::ALS_CONF_0, (uint8_t *) conf.raw_bytes, VEML_REG_SIZE);
295 if (err != i2c::ERROR_OK) {
296 ESP_LOGW(TAG, "Error reading ALS_CONF_0 register, err = %d", white_err);
297 }
298 data.actual_time = conf.ALS_IT;
299 data.actual_gain = conf.ALS_GAIN;
300
301 ESP_LOGV(TAG, "Data from sensors: ALS = %d, WHITE = %d, Gain = %s, Time = %d ms", data.als_counts, data.white_counts,
302 get_gain_str(data.actual_gain), get_itime_ms(data.actual_time));
303 return std::max(als_err, white_err);
304}
305
307 // skip first sample in auto mode -
308 // we need to reconfigure device after last measurement
309 if (this->state_ == State::COLLECTING_DATA_AUTO)
310 return true;
311
312 if (!this->automatic_mode_enabled_)
313 return false;
314
315 // Recommended thresholds as per datasheet
316 static constexpr uint16_t LOW_INTENSITY_THRESHOLD = 100;
317 static constexpr uint16_t HIGH_INTENSITY_THRESHOLD = 10000;
318
322 static const Gain GAINS[GAINS_COUNT] = {X_1_8, X_1_4, X_1, X_2};
323
324 if (data.als_counts <= LOW_INTENSITY_THRESHOLD) {
325 Gain next_gain = get_next(GAINS, data.actual_gain);
326 if (next_gain != data.actual_gain) {
327 data.actual_gain = next_gain;
328 return true;
329 }
330 IntegrationTime next_time = get_next(TIMES, data.actual_time);
331 if (next_time != data.actual_time) {
332 data.actual_time = next_time;
333 return true;
334 }
335 } else if (data.als_counts >= HIGH_INTENSITY_THRESHOLD) {
336 Gain prev_gain = get_prev(GAINS, data.actual_gain);
337 if (prev_gain != data.actual_gain) {
338 data.actual_gain = prev_gain;
339 return true;
340 }
341 IntegrationTime prev_time = get_prev(TIMES, data.actual_time);
342 if (prev_time != data.actual_time) {
343 data.actual_time = prev_time;
344 return true;
345 }
346 }
347
348 // Counts are either good (between thresholds)
349 // or there is no room to change sensitivity anymore
350 return false;
351}
352
354 static const float MAX_GAIN = 2.0f;
355 static const float MAX_ITIME_MS = 800.0f;
356 static const float MAX_LX_RESOLUTION = 0.0036f;
357 float lux_resolution = (MAX_ITIME_MS / (float) get_itime_ms(data.actual_time)) *
358 (MAX_GAIN / get_gain_coeff(data.actual_gain)) * MAX_LX_RESOLUTION;
359 ESP_LOGV(TAG, "Lux resolution for (%d, %s) = %.4f ", get_itime_ms(data.actual_time), get_gain_str(data.actual_gain),
360 lux_resolution);
361
362 data.als_lux = lux_resolution * (float) data.als_counts;
363 data.white_lux = lux_resolution * (float) data.white_counts;
364 data.fake_infrared_lux = reduce_to_zero(data.white_lux, data.als_lux);
365
366 ESP_LOGV(TAG, "%s mode - ALS = %.1f lx, WHITE = %.1f lx, FAKE_IR = %.1f lx",
367 this->automatic_mode_enabled_ ? "Automatic" : "Manual", data.als_lux, data.white_lux,
368 data.fake_infrared_lux);
369}
370
372 if (!this->lux_compensation_enabled_)
373 return;
374 auto &local_data = data;
375 // Always apply correction for G1/4 and G1/8
376 // Other Gains G1 and G2 are not supposed to be used for lux > 1000,
377 // corrections may help, but not a lot.
378 //
379 // "Illumination values higher than 1000 lx show non-linearity.
380 // This non-linearity is the same for all sensors, so a compensation formula can be applied
381 // if this light level is exceeded"
382 auto compensate = [&local_data](float &lux) {
383 auto calculate_high_lux_compensation = [](float lux_veml) -> float {
384 return (((6.0135e-13 * lux_veml - 9.3924e-9) * lux_veml + 8.1488e-5) * lux_veml + 1.0023) * lux_veml;
385 };
386
387 if (lux > 1000.0f || local_data.actual_gain == Gain::X_1_8 || local_data.actual_gain == Gain::X_1_4) {
388 lux = calculate_high_lux_compensation(lux);
389 }
390 };
391
392 compensate(data.als_lux);
393 compensate(data.white_lux);
394 data.fake_infrared_lux = reduce_to_zero(data.white_lux, data.als_lux);
395
396 ESP_LOGV(TAG, "Lux compensation - ALS = %.1f lx, WHITE = %.1f lx, FAKE_IR = %.1f lx", data.als_lux, data.white_lux,
397 data.fake_infrared_lux);
398}
399
401 data.als_lux *= this->glass_attenuation_factor_;
403 data.fake_infrared_lux = reduce_to_zero(data.white_lux, data.als_lux);
404 ESP_LOGV(TAG, "Glass attenuation - ALS = %.1f lx, WHITE = %.1f lx, FAKE_IR = %.1f lx", data.als_lux, data.white_lux,
405 data.fake_infrared_lux);
406}
407
409 if (this->ambient_light_sensor_ != nullptr) {
411 }
412 if (this->white_sensor_ != nullptr) {
414 }
415}
416
418 if (this->fake_infrared_sensor_ != nullptr) {
420 }
421 if (this->ambient_light_counts_sensor_ != nullptr) {
423 }
424 if (this->white_counts_sensor_ != nullptr) {
426 }
427}
428
430 if (this->actual_gain_sensor_ != nullptr) {
431 this->actual_gain_sensor_->publish_state(get_gain_coeff(data.actual_gain));
432 }
433 if (this->actual_integration_time_sensor_ != nullptr) {
434 this->actual_integration_time_sensor_->publish_state(get_itime_ms(data.actual_time));
435 }
436}
437} // namespace veml7700
438} // namespace esphome
virtual void mark_failed()
Mark this component as failed.
bool is_failed() const
void status_set_warning(const char *message=nullptr)
bool is_ready() const
void status_clear_warning()
void set_timeout(const std::string &name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a unique name.
ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len) const
writes an array of bytes to a specific register in the I²C device
Definition i2c.cpp:44
ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len)
reads an array of bytes from a specific register in the I²C device
Definition i2c.cpp:35
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:75
void publish_data_part_1_(Readings &data)
Definition veml7700.cpp:408
void publish_data_part_2_(Readings &data)
Definition veml7700.cpp:417
void publish_data_part_3_(Readings &data)
Definition veml7700.cpp:429
sensor::Sensor * white_counts_sensor_
Definition veml7700.h:193
void apply_glass_attenuation_(Readings &data)
Definition veml7700.cpp:400
sensor::Sensor * ambient_light_counts_sensor_
Definition veml7700.h:191
bool are_adjustments_required_(Readings &data)
Definition veml7700.cpp:306
ErrorCode reconfigure_time_and_gain_(IntegrationTime time, Gain gain, bool shutdown)
Definition veml7700.cpp:261
sensor::Sensor * actual_integration_time_sensor_
Definition veml7700.h:196
void apply_lux_calculation_(Readings &data)
Definition veml7700.cpp:353
struct esphome::veml7700::VEML7700Component::Readings readings_
sensor::Sensor * fake_infrared_sensor_
Definition veml7700.h:194
void apply_lux_compensation_(Readings &data)
Definition veml7700.cpp:371
sensor::Sensor * ambient_light_sensor_
Definition veml7700.h:190
ErrorCode read_sensor_output_(Readings &data)
Definition veml7700.cpp:282
AlsGain501 gain
mopeka_std_values val[4]
ErrorCode
Error codes returned by I2CBus and I2CDevice methods.
Definition i2c_bus.h:31
@ ERROR_OK
No error found during execution of method.
Definition i2c_bus.h:33
T get_prev(const T(&array)[size], const T val)
Definition veml7700.cpp:29
T get_next(const T(&array)[size], const T val)
Definition veml7700.cpp:14
const uint8_t INTEGRATION_TIMES_COUNT
Definition veml7700.h:42
const uint8_t GAINS_COUNT
Definition veml7700.h:32
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
void IRAM_ATTR HOT delay(uint32_t ms)
Definition core.cpp:31