ESPHome 2026.5.1
Loading...
Searching...
No Matches
ltr_als_ps.cpp
Go to the documentation of this file.
1#include "ltr_als_ps.h"
4#include "esphome/core/log.h"
5#include <limits>
6
8
10
11static const char *const TAG = "ltr_als_ps";
12
13static const uint8_t MAX_TRIES = 5;
14
15template<typename T, size_t size> T get_next(const T (&array)[size], const T val) {
16 size_t i = 0;
17 size_t idx = std::numeric_limits<size_t>::max();
18 while (idx == std::numeric_limits<size_t>::max() && i < size) {
19 if (array[i] == val) {
20 idx = i;
21 break;
22 }
23 i++;
24 }
25 if (idx == std::numeric_limits<size_t>::max() || i + 1 >= size)
26 return val;
27 return array[i + 1];
28}
29
30template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) {
31 size_t i = size - 1;
32 size_t idx = std::numeric_limits<size_t>::max();
33 while (idx == std::numeric_limits<size_t>::max() && i > 0) {
34 if (array[i] == val) {
35 idx = i;
36 break;
37 }
38 i--;
39 }
40 if (idx == std::numeric_limits<size_t>::max() || i == 0)
41 return val;
42 return array[i - 1];
43}
44
45static uint16_t get_itime_ms(IntegrationTime time) {
46 static const uint16_t ALS_INT_TIME[8] = {100, 50, 200, 400, 150, 250, 300, 350};
47 return ALS_INT_TIME[time & 0b111];
48}
49
50static uint16_t get_meas_time_ms(MeasurementRepeatRate rate) {
51 static const uint16_t ALS_MEAS_RATE[8] = {50, 100, 200, 500, 1000, 2000, 2000, 2000};
52 return ALS_MEAS_RATE[rate & 0b111];
53}
54
55static float get_gain_coeff(AlsGain gain) {
56 static const float ALS_GAIN[8] = {1, 2, 4, 8, 0, 0, 48, 96};
57 return ALS_GAIN[gain & 0b111];
58}
59
60static float get_ps_gain_coeff(PsGain gain) {
61 static const float PS_GAIN[4] = {16, 0, 32, 64};
62 return PS_GAIN[gain & 0b11];
63}
64
66 // As per datasheet we need to wait at least 100ms after power on to get ALS chip responsive
67 this->set_timeout(100, [this]() { this->state_ = State::DELAYED_SETUP; });
68}
69
71 auto get_device_type = [](LtrType typ) {
72 switch (typ) {
74 return "ALS only";
76 return "PS only";
78 return "ALS + PS";
79 default:
80 return "Unknown";
81 }
82 };
83
84 LOG_I2C_DEVICE(this);
85 ESP_LOGCONFIG(TAG, " Device type: %s", get_device_type(this->ltr_type_));
86 if (this->is_als_()) {
87 ESP_LOGCONFIG(TAG,
88 " Automatic mode: %s\n"
89 " Gain: %.0fx\n"
90 " Integration time: %d ms\n"
91 " Measurement repeat rate: %d ms\n"
92 " Glass attenuation factor: %f",
93 ONOFF(this->automatic_mode_enabled_), get_gain_coeff(this->gain_),
94 get_itime_ms(this->integration_time_), get_meas_time_ms(this->repeat_rate_),
96 LOG_SENSOR(" ", "ALS calculated lux", this->ambient_light_sensor_);
97 LOG_SENSOR(" ", "CH1 Infrared counts", this->infrared_counts_sensor_);
98 LOG_SENSOR(" ", "CH0 Visible+IR counts", this->full_spectrum_counts_sensor_);
99 LOG_SENSOR(" ", "Actual gain", this->actual_gain_sensor_);
100 }
101 if (this->is_ps_()) {
102 ESP_LOGCONFIG(TAG,
103 " Proximity gain: %.0fx\n"
104 " Proximity cooldown time: %d s\n"
105 " Proximity high threshold: %d\n"
106 " Proximity low threshold: %d",
107 get_ps_gain_coeff(this->ps_gain_), this->ps_cooldown_time_s_, this->ps_threshold_high_,
108 this->ps_threshold_low_);
109 LOG_SENSOR(" ", "Proximity counts", this->proximity_counts_sensor_);
110 }
111 LOG_UPDATE_INTERVAL(this);
112
113 if (this->is_failed()) {
114 ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
115 }
116}
117
119 ESP_LOGV(TAG, "Updating");
120 if (this->is_ready() && this->state_ == State::IDLE) {
121 ESP_LOGV(TAG, "Initiating new data collection");
122
124
125 this->als_readings_.ch0 = 0;
126 this->als_readings_.ch1 = 0;
127 this->als_readings_.gain = this->gain_;
129 this->als_readings_.lux = 0;
131
132 } else {
133 ESP_LOGV(TAG, "Component not ready yet");
134 }
135}
136
139
140 switch (this->state_) {
142 err = this->write(nullptr, 0);
143 if (err != i2c::ERROR_OK) {
144 ESP_LOGV(TAG, "i2c connection failed");
145 this->mark_failed();
146 }
147 this->configure_reset_();
148 if (this->is_als_()) {
149 this->configure_als_();
151 }
152 if (this->is_ps_()) {
153 this->configure_ps_();
154 }
155
156 this->state_ = State::IDLE;
157 break;
158
159 case State::IDLE:
160 if (this->is_ps_()) {
162 }
163 break;
164
167 this->read_data_tries_ = 0;
168 ESP_LOGV(TAG, "Reading sensor data having gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain),
169 get_itime_ms(this->als_readings_.integration_time));
170 this->read_sensor_data_(this->als_readings_);
171 this->state_ = State::DATA_COLLECTED;
173 } else if (this->read_data_tries_ >= MAX_TRIES) {
174 ESP_LOGW(TAG, "Can't get data after several tries.");
175 this->read_data_tries_ = 0;
176 this->status_set_warning();
177 this->state_ = State::IDLE;
178 return;
179 } else {
180 this->read_data_tries_++;
181 }
182 break;
183
186 // first measurement in auto mode (COLLECTING_DATA_AUTO state) require device reconfiguration
187 if (this->state_ == State::COLLECTING_DATA_AUTO || this->are_adjustments_required_(this->als_readings_)) {
188 this->state_ = State::ADJUSTMENT_IN_PROGRESS;
189 ESP_LOGD(TAG, "Reconfiguring sensitivity: gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain),
190 get_itime_ms(this->als_readings_.integration_time));
193 // if sensitivity adjustment needed - need to wait for first data samples after setting new parameters
194 this->set_timeout(2 * get_meas_time_ms(this->repeat_rate_),
195 [this]() { this->state_ = State::WAITING_FOR_DATA; });
196 } else {
197 this->state_ = State::READY_TO_PUBLISH;
198 }
199 break;
200
202 // nothing to be done, just waiting for the timeout
203 break;
204
207 this->state_ = State::KEEP_PUBLISHING;
208 break;
209
212 this->status_clear_warning();
213 this->state_ = State::IDLE;
214 break;
215
216 default:
217 break;
218 }
219}
220
222 uint16_t ps_data = this->read_ps_data_();
223 uint32_t now = millis();
224
225 if (ps_data != this->ps_readings_) {
226 this->ps_readings_ = ps_data;
227 // Higher values - object is closer to sensor
228 if (ps_data > this->ps_threshold_high_ &&
229 now - this->last_ps_high_trigger_time_ >= this->ps_cooldown_time_s_ * 1000) {
230 this->last_ps_high_trigger_time_ = now;
231 ESP_LOGV(TAG, "Proximity high threshold triggered. Value = %d, Trigger level = %d", ps_data,
232 this->ps_threshold_high_);
233 this->on_ps_high_trigger_callback_.call();
234 } else if (ps_data < this->ps_threshold_low_ &&
235 now - this->last_ps_low_trigger_time_ >= this->ps_cooldown_time_s_ * 1000) {
236 this->last_ps_low_trigger_time_ = now;
237 ESP_LOGV(TAG, "Proximity low threshold triggered. Value = %d, Trigger level = %d", ps_data,
238 this->ps_threshold_low_);
239 this->on_ps_low_trigger_callback_.call();
240 }
241 }
242}
243
245 uint8_t manuf_id = this->reg((uint8_t) CommandRegisters::MANUFAC_ID).get();
246 if (manuf_id != 0x05) { // 0x05 is Lite-On Semiconductor Corp. ID
247 ESP_LOGW(TAG, "Unknown manufacturer ID: 0x%02X", manuf_id);
248 this->mark_failed();
249 return false;
250 }
251
252 // Things getting not really funny here, we can't identify device type by part number ID
253 // ======================== ========= ===== =================
254 // Device Part ID Rev Capabilities
255 // ======================== ========= ===== =================
256 // Ltr-329/ltr-303 0x0a 0x00 Als 16b
257 // Ltr-553/ltr-556/ltr-556 0x09 0x02 Als 16b + Ps 11b diff nm sens
258 // Ltr-659 0x09 0x02 Ps 11b and ps gain
259 //
260 // There are other devices which might potentially work with default settings,
261 // but registers layout is different and we can't use them properly. For ex. ltr-558
262
263 PartIdRegister part_id{0};
264 part_id.raw = this->reg((uint8_t) CommandRegisters::PART_ID).get();
265 if (part_id.part_number_id != 0x0a && part_id.part_number_id != 0x09) {
266 ESP_LOGW(TAG, "Unknown part number ID: 0x%02X. It might not work properly.", part_id.part_number_id);
267 this->status_set_warning();
268 return true;
269 }
270 return true;
271}
272
274 ESP_LOGV(TAG, "Resetting");
275
276 AlsControlRegister als_ctrl{0};
277 als_ctrl.sw_reset = true;
278 this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw;
279 delay(2);
280
281 uint8_t tries = MAX_TRIES;
282 do {
283 ESP_LOGV(TAG, "Waiting for chip to reset");
284 delay(2);
285 als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get();
286 } while (als_ctrl.sw_reset && tries--); // while sw reset bit is on - keep waiting
287
288 if (als_ctrl.sw_reset) {
289 ESP_LOGW(TAG, "Reset timed out");
290 }
291}
292
294 AlsControlRegister als_ctrl{0};
295
296 als_ctrl.sw_reset = false;
297 als_ctrl.active_mode = true;
298 als_ctrl.gain = this->gain_;
299
300 ESP_LOGV(TAG, "Setting active mode and gain reg 0x%02X", als_ctrl.raw);
301 this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw;
302 delay(5);
303
304 uint8_t tries = MAX_TRIES;
305 do {
306 ESP_LOGV(TAG, "Waiting for device to become active");
307 delay(2);
308 als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get();
309 } while (!als_ctrl.active_mode && tries--); // while active mode is not set - keep waiting
310
311 if (!als_ctrl.active_mode) {
312 ESP_LOGW(TAG, "Failed to activate device");
313 }
314}
315
317 PsMeasurementRateRegister ps_meas{0};
319 this->reg((uint8_t) CommandRegisters::PS_MEAS_RATE) = ps_meas.raw;
320
321 PsControlRegister ps_ctrl{0};
322 ps_ctrl.ps_mode_active = true;
323 ps_ctrl.ps_mode_xxx = true;
324 this->reg((uint8_t) CommandRegisters::PS_CONTR) = ps_ctrl.raw;
325}
326
328 AlsPsStatusRegister als_status{0};
329 als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get();
330 if (!als_status.ps_new_data || als_status.data_invalid) {
331 return this->ps_readings_;
332 }
333
334 uint8_t ps_low = this->reg((uint8_t) CommandRegisters::PS_DATA_0).get();
335 PsData1Register ps_high;
336 ps_high.raw = this->reg((uint8_t) CommandRegisters::PS_DATA_1).get();
337
338 uint16_t val = encode_uint16(ps_high.ps_data_high, ps_low);
339 if (ps_high.ps_saturation_flag) {
340 return 0x7ff; // full 11 bit range
341 }
342 return val;
343}
344
346 AlsControlRegister als_ctrl{0};
347 als_ctrl.active_mode = true;
348 als_ctrl.gain = gain;
349 this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw;
350 delay(2);
351
352 AlsControlRegister read_als_ctrl{0};
353 read_als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get();
354 if (read_als_ctrl.gain != gain) {
355 ESP_LOGW(TAG, "Failed to set gain. We will try one more time.");
356 this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw;
357 delay(2);
358 }
359}
360
364 meas.integration_time = time;
365 this->reg((uint8_t) CommandRegisters::MEAS_RATE) = meas.raw;
366 delay(2);
367
368 MeasurementRateRegister read_meas{0};
369 read_meas.raw = this->reg((uint8_t) CommandRegisters::MEAS_RATE).get();
370 if (read_meas.integration_time != time) {
371 ESP_LOGW(TAG, "Failed to set integration time. We will try one more time.");
372 this->reg((uint8_t) CommandRegisters::MEAS_RATE) = meas.raw;
373 delay(2);
374 }
375}
376
378 AlsPsStatusRegister als_status{0};
379
380 als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get();
381 if (!als_status.als_new_data)
383
384 if (als_status.data_invalid) {
385 ESP_LOGW(TAG, "Data available but not valid");
387 }
388 ESP_LOGV(TAG, "Data ready, reported gain is %.0f", get_gain_coeff(als_status.gain));
389 if (data.gain != als_status.gain) {
390 ESP_LOGW(TAG, "Actual gain differs from requested (%.0f)", get_gain_coeff(data.gain));
392 }
394}
395
397 data.ch1 = 0;
398 data.ch0 = 0;
399 uint8_t ch1_0 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH1_0).get();
400 uint8_t ch1_1 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH1_1).get();
401 uint8_t ch0_0 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH0_0).get();
402 uint8_t ch0_1 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH0_1).get();
403 data.ch1 = encode_uint16(ch1_1, ch1_0);
404 data.ch0 = encode_uint16(ch0_1, ch0_0);
405
406 ESP_LOGV(TAG, "Got sensor data: CH1 = %d, CH0 = %d", data.ch1, data.ch0);
407}
408
410 if (!this->automatic_mode_enabled_)
411 return false;
412
413 if (data.number_of_adjustments > 15) {
414 // sometimes sensors fail to change sensitivity. this prevents us from infinite loop
415 ESP_LOGW(TAG, "Too many sensitivity adjustments done. Apparently, sensor reconfiguration fails. Stopping.");
416 return false;
417 }
418 data.number_of_adjustments++;
419
420 // Recommended thresholds as per datasheet
421 static const uint16_t LOW_INTENSITY_THRESHOLD = 1000;
422 static const uint16_t HIGH_INTENSITY_THRESHOLD = 30000;
423 static const AlsGain GAINS[GAINS_COUNT] = {GAIN_1, GAIN_2, GAIN_4, GAIN_8, GAIN_48, GAIN_96};
424 static const IntegrationTime INT_TIMES[TIMES_COUNT] = {
427
428 if (data.ch0 <= LOW_INTENSITY_THRESHOLD) {
429 AlsGain next_gain = get_next(GAINS, data.gain);
430 if (next_gain != data.gain) {
431 data.gain = next_gain;
432 ESP_LOGV(TAG, "Low illuminance. Increasing gain.");
433 return true;
434 }
435 IntegrationTime next_time = get_next(INT_TIMES, data.integration_time);
436 if (next_time != data.integration_time) {
437 data.integration_time = next_time;
438 ESP_LOGV(TAG, "Low illuminance. Increasing integration time.");
439 return true;
440 }
441 } else if (data.ch0 >= HIGH_INTENSITY_THRESHOLD) {
442 AlsGain prev_gain = get_prev(GAINS, data.gain);
443 if (prev_gain != data.gain) {
444 data.gain = prev_gain;
445 ESP_LOGV(TAG, "High illuminance. Decreasing gain.");
446 return true;
447 }
448 IntegrationTime prev_time = get_prev(INT_TIMES, data.integration_time);
449 if (prev_time != data.integration_time) {
450 data.integration_time = prev_time;
451 ESP_LOGV(TAG, "High illuminance. Decreasing integration time.");
452 return true;
453 }
454 } else {
455 ESP_LOGD(TAG, "Illuminance is sufficient.");
456 return false;
457 }
458 ESP_LOGD(TAG, "Can't adjust sensitivity anymore.");
459 return false;
460}
461
463 if ((data.ch0 == 0xFFFF) || (data.ch1 == 0xFFFF)) {
464 ESP_LOGW(TAG, "Sensors got saturated");
465 data.lux = 0.0f;
466 return;
467 }
468
469 if ((data.ch0 == 0x0000) && (data.ch1 == 0x0000)) {
470 ESP_LOGW(TAG, "Sensors blacked out");
471 data.lux = 0.0f;
472 return;
473 }
474
475 float ch0 = data.ch0;
476 float ch1 = data.ch1;
477 float ratio = ch1 / (ch0 + ch1);
478 float als_gain = get_gain_coeff(data.gain);
479 float als_time = ((float) get_itime_ms(data.integration_time)) / 100.0f;
480 float inv_pfactor = this->glass_attenuation_factor_;
481 float lux = 0.0f;
482
483 if (ratio < 0.45) {
484 lux = (1.7743 * ch0 + 1.1059 * ch1);
485 } else if (ratio < 0.64 && ratio >= 0.45) {
486 lux = (4.2785 * ch0 - 1.9548 * ch1);
487 } else if (ratio < 0.85 && ratio >= 0.64) {
488 lux = (0.5926 * ch0 + 0.1185 * ch1);
489 } else {
490 ESP_LOGW(TAG, "Impossible ch1/(ch0 + ch1) ratio");
491 lux = 0.0f;
492 }
493 lux = inv_pfactor * lux / als_gain / als_time;
494 data.lux = lux;
495
496 ESP_LOGV(TAG, "Lux calculation: ratio %.3f, gain %.0fx, int time %.1f, inv_pfactor %.3f, lux %.3f", ratio, als_gain,
497 als_time, inv_pfactor, lux);
498}
499
501 if (this->proximity_counts_sensor_ != nullptr) {
503 }
504 if (this->ambient_light_sensor_ != nullptr) {
506 }
507 if (this->infrared_counts_sensor_ != nullptr) {
509 }
510 if (this->full_spectrum_counts_sensor_ != nullptr) {
512 }
513}
514
516 if (this->actual_gain_sensor_ != nullptr) {
517 this->actual_gain_sensor_->publish_state(get_gain_coeff(data.gain));
518 }
519 if (this->actual_integration_time_sensor_ != nullptr) {
520 this->actual_integration_time_sensor_->publish_state(get_itime_ms(data.integration_time));
521 }
522}
523} // namespace esphome::ltr_als_ps
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:284
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_timeout(const std voi set_timeout)(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a unique name.
Definition component.h:510
bool is_ready() const
void status_clear_warning()
Definition component.h:306
ErrorCode write(const uint8_t *data, size_t len) const
writes an array of bytes to a device using an I2CBus
Definition i2c.h:183
I2CRegister reg(uint8_t a_register)
calls the I2CRegister constructor
Definition i2c.h:152
uint8_t get() const
returns the register value
Definition i2c.cpp:84
void publish_data_part_2_(AlsReadings &data)
void configure_integration_time_(IntegrationTime time)
sensor::Sensor * full_spectrum_counts_sensor_
Definition ltr_als_ps.h:148
LtrDataAvail is_als_data_ready_(AlsReadings &data)
CallbackManager< void()> on_ps_low_trigger_callback_
Definition ltr_als_ps.h:162
void read_sensor_data_(AlsReadings &data)
CallbackManager< void()> on_ps_high_trigger_callback_
Definition ltr_als_ps.h:161
void publish_data_part_1_(AlsReadings &data)
bool are_adjustments_required_(AlsReadings &data)
struct esphome::ltr_als_ps::LTRAlsPsComponent::AlsReadings als_readings_
sensor::Sensor * actual_integration_time_sensor_
Definition ltr_als_ps.h:151
void apply_lux_calculation_(AlsReadings &data)
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:68
AlsGain501 gain
mopeka_std_values val[3]
ErrorCode
Error codes returned by I2CBus and I2CDevice methods.
Definition i2c_bus.h:12
@ ERROR_OK
No error found during execution of method.
Definition i2c_bus.h:14
T get_prev(const T(&array)[size], const T val)
T get_next(const T(&array)[size], const T val)
uint16_t size
Definition helpers.cpp:25
constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb)
Encode a 16-bit value given the most and least significant byte.
Definition helpers.h:859
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
static void uint32_t
Gain ALS_GAIN
Definition veml7700.h:9