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