ESPHome 2026.6.0-dev
Loading...
Searching...
No Matches
tsl2591.cpp
Go to the documentation of this file.
1#include "tsl2591.h"
2#include "esphome/core/log.h"
3#include "esphome/core/hal.h"
4
5namespace esphome::tsl2591 {
6
7static const char *const TAG = "tsl2591.sensor";
8
9// Various constants used in TSL2591 register manipulation
10#define TSL2591_COMMAND_BIT (0xA0) // 1010 0000: bits 7 and 5 for 'command, normal'
11#define TSL2591_ENABLE_POWERON (0x01) // Flag for ENABLE register, to enable
12#define TSL2591_ENABLE_POWEROFF (0x00) // Flag for ENABLE register, to disable
13#define TSL2591_ENABLE_AEN (0x02) // Flag for ENABLE register, to turn on ADCs
14
15// TSL2591 registers from the datasheet. We only define what we use.
16#define TSL2591_REGISTER_ENABLE (0x00)
17#define TSL2591_REGISTER_CONTROL (0x01)
18#define TSL2591_REGISTER_DEVICE_ID (0x12)
19#define TSL2591_REGISTER_STATUS (0x13)
20#define TSL2591_REGISTER_CHAN0_LOW (0x14)
21#define TSL2591_REGISTER_CHAN0_HIGH (0x15)
22#define TSL2591_REGISTER_CHAN1_LOW (0x16)
23#define TSL2591_REGISTER_CHAN1_HIGH (0x17)
24
26 // Enable the device by setting the control bit to 0x01. Also turn on ADCs.
27 if (!this->write_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_ENABLE, TSL2591_ENABLE_POWERON | TSL2591_ENABLE_AEN)) {
28 ESP_LOGE(TAG, "I2C write failed");
29 }
30}
31
33 if (!this->write_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_ENABLE, TSL2591_ENABLE_POWEROFF)) {
34 ESP_LOGE(TAG, "I2C write failed");
35 }
36}
37
43
45 switch (this->component_gain_) {
47 this->gain_ = TSL2591_GAIN_LOW;
48 break;
50 this->gain_ = TSL2591_GAIN_MED;
51 break;
54 break;
56 this->gain_ = TSL2591_GAIN_MAX;
57 break;
59 this->gain_ = TSL2591_GAIN_MED;
60 break;
61 }
62
63 uint8_t id;
64 if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_DEVICE_ID, &id)) {
65 ESP_LOGE(TAG, "I2C read failed");
66 this->mark_failed();
67 return;
68 }
69
70 if (id != 0x50) {
71 ESP_LOGE(TAG, "Unknown chip ID");
72 this->mark_failed();
73 return;
74 }
75
78}
79
81 ESP_LOGCONFIG(TAG, "TSL2591:");
82 LOG_I2C_DEVICE(this);
83
84 if (this->is_failed()) {
85 ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
86 return;
87 }
88
89 ESP_LOGCONFIG(TAG, " Name: %s", this->name_);
91 int gain = 0;
92 std::string gain_word = "unknown";
93 switch (raw_gain) {
95 gain = 1;
96 gain_word = "low";
97 break;
99 gain = 25;
100 gain_word = "medium";
101 break;
103 gain = 400;
104 gain_word = "high";
105 break;
107 gain = 9500;
108 gain_word = "maximum";
109 break;
111 gain = -1;
112 gain_word = "auto";
113 break;
114 }
115 TSL2591IntegrationTime raw_timing = this->integration_time_;
116 int timing_ms = (1 + raw_timing) * 100;
117 ESP_LOGCONFIG(TAG,
118 " Gain: %dx (%s)"
119 " Integration Time: %d ms\n"
120 " Power save mode enabled: %s\n"
121 " Device factor: %f\n"
122 " Glass attenuation factor: %f",
123 gain, gain_word.c_str(), timing_ms, ONOFF(this->power_save_mode_enabled_), this->device_factor_,
125 LOG_SENSOR(" ", "Full spectrum:", this->full_spectrum_sensor_);
126 LOG_SENSOR(" ", "Infrared:", this->infrared_sensor_);
127 LOG_SENSOR(" ", "Visible:", this->visible_sensor_);
128 LOG_SENSOR(" ", "Calculated lux:", this->calculated_lux_sensor_);
129 LOG_SENSOR(" ", "Actual gain:", this->actual_gain_sensor_);
130
131 LOG_UPDATE_INTERVAL(this);
132}
133
135 uint32_t combined = this->get_combined_illuminance();
136 uint16_t visible = this->get_illuminance(TSL2591_SENSOR_CHANNEL_VISIBLE, combined);
137 uint16_t infrared = this->get_illuminance(TSL2591_SENSOR_CHANNEL_INFRARED, combined);
138 uint16_t full = this->get_illuminance(TSL2591_SENSOR_CHANNEL_FULL_SPECTRUM, combined);
139 float lux = this->get_calculated_lux(full, infrared);
140 uint16_t actual_gain = this->get_actual_gain();
141 ESP_LOGD(TAG, "Got illuminance: combined 0x%" PRIX32 ", full %d, IR %d, vis %d. Calc lux: %f. Actual gain: %d.",
142 combined, full, infrared, visible, lux, actual_gain);
143 if (this->full_spectrum_sensor_ != nullptr) {
145 }
146 if (this->infrared_sensor_ != nullptr) {
147 this->infrared_sensor_->publish_state(infrared);
148 }
149 if (this->visible_sensor_ != nullptr) {
150 this->visible_sensor_->publish_state(visible);
151 }
152 if (this->calculated_lux_sensor_ != nullptr) {
154 }
155
156 if (this->component_gain_ == TSL2591_CGAIN_AUTO) {
157 this->automatic_gain_update(full);
158 }
159
160 if (this->actual_gain_sensor_ != nullptr) {
161 this->actual_gain_sensor_->publish_state(actual_gain);
162 }
163 this->status_clear_warning();
164}
165
166#define interval_name "tsl2591_interval_for_update"
167
169 if (!this->is_adc_valid()) {
170 uint64_t now = millis();
171 ESP_LOGD(TAG, "Elapsed %3llu ms; still waiting for valid ADC", (now - this->interval_start_));
172 if (now > this->interval_timeout_) {
173 ESP_LOGW(TAG, "Interval timeout for '%s' expired before ADCs became valid", this->name_);
174 this->cancel_interval(interval_name);
175 }
176 return;
177 }
178 this->cancel_interval(interval_name);
179 this->process_update_();
180}
181
183 if (!is_failed()) {
184 if (this->power_save_mode_enabled_) {
185 // we enabled it here, else ADC will never become valid
186 // but actually doing the reads will disable device if needed
187 this->enable();
188 }
189 if (this->is_adc_valid()) {
190 this->process_update_();
191 } else {
192 this->interval_start_ = millis();
193 this->interval_timeout_ = this->interval_start_ + 620;
194 this->set_interval(interval_name, 100, [this] { this->interval_function_for_update_(); });
195 }
196 }
197}
198
200 this->infrared_sensor_ = infrared_sensor;
201}
202
203void TSL2591Component::set_visible_sensor(sensor::Sensor *visible_sensor) { this->visible_sensor_ = visible_sensor; }
204
206 this->full_spectrum_sensor_ = full_spectrum_sensor;
207}
208
210 this->calculated_lux_sensor_ = calculated_lux_sensor;
211}
212
214 this->actual_gain_sensor_ = actual_gain_sensor;
215}
216
220
222
223void TSL2591Component::set_device_and_glass_attenuation_factors(float device_factor, float glass_attenuation_factor) {
224 this->device_factor_ = device_factor;
225 this->glass_attenuation_factor_ = glass_attenuation_factor;
226}
227
229 this->enable();
231 this->gain_ = gain;
232 if (!this->write_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CONTROL,
233 static_cast<uint8_t>(this->integration_time_) | static_cast<uint8_t>(this->gain_))) {
234 ESP_LOGE(TAG, "I2C write failed");
235 }
236 // The ADC values can be confused if gain or integration time are changed in the middle of a cycle.
237 // So, we unconditionally disable the device to turn the ADCs off. When re-enabling, the ADCs
238 // will tell us when they are ready again. That avoids an initial bogus reading.
239 this->disable();
240 if (!this->power_save_mode_enabled_) {
241 this->enable();
242 }
243}
244
246
247void TSL2591Component::set_name(const char *name) { this->name_ = name; }
248
250 uint8_t status;
251 if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_STATUS, &status)) {
252 ESP_LOGE(TAG, "I2C read failed");
253 return false;
254 }
255 return status & 0x01;
256}
257
259 this->enable();
260 // Wait x ms for ADC to complete and signal valid.
261 // The max integration time is 600ms, so that's our max delay.
262 // (But we use 620ms as a bit of slack.)
263 // We'll do mini-delays and break out as soon as the ADC is good.
264 bool avalid;
265 const uint8_t mini_delay = 100;
266 for (uint16_t d = 0; d < 620; d += mini_delay) {
267 avalid = this->is_adc_valid();
268 if (avalid) {
269 break;
270 }
271 // we only log this if we need any delay, since normally we don't
272 ESP_LOGD(TAG, " after %3d ms: ADC valid? %s", d, avalid ? "true" : "false");
273 delay(mini_delay);
274 }
275 if (!avalid) {
276 // still not valid after a sutiable delay
277 // we don't mark the device as failed since it might come around in the future (probably not :-()
278 ESP_LOGE(TAG, "Device '%s' returned invalid readings", this->name_);
280 return 0;
281 }
282
283 // CHAN0 must be read before CHAN1
284 // See: https://forums.adafruit.com/viewtopic.php?f=19&t=124176
285 // Also, low byte must be read before high byte..
286 // We read the registers in the order described in the datasheet.
287 uint32_t x32;
288 uint8_t ch0low, ch0high, ch1low, ch1high;
289 uint16_t ch0_16;
290 uint16_t ch1_16;
291 if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CHAN0_LOW, &ch0low)) {
292 ESP_LOGE(TAG, "I2C read failed");
293 return 0;
294 }
295 if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CHAN0_HIGH, &ch0high)) {
296 ESP_LOGE(TAG, "I2C read failed");
297 return 0;
298 }
299 ch0_16 = (ch0high << 8) | ch0low;
300 if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CHAN1_LOW, &ch1low)) {
301 ESP_LOGE(TAG, "I2C read failed");
302 return 0;
303 }
304 if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CHAN1_HIGH, &ch1high)) {
305 ESP_LOGE(TAG, "I2C read failed");
306 return 0;
307 }
308 ch1_16 = (ch1high << 8) | ch1low;
309 x32 = (ch1_16 << 16) | ch0_16;
310
312 return x32;
313}
314
316 uint32_t combined = this->get_combined_illuminance();
317 return this->get_illuminance(channel, combined);
318}
319// logic cloned from Adafruit TSL2591 library
320uint16_t TSL2591Component::get_illuminance(TSL2591SensorChannel channel, uint32_t combined_illuminance) {
322 // Reads two byte value from channel 0 (visible + infrared)
323 return (combined_illuminance & 0xFFFF);
324 } else if (channel == TSL2591_SENSOR_CHANNEL_INFRARED) {
325 // Reads two byte value from channel 1 (infrared)
326 return (combined_illuminance >> 16);
327 } else if (channel == TSL2591_SENSOR_CHANNEL_VISIBLE) {
328 // Reads all and subtracts out the infrared
329 uint16_t full = combined_illuminance & 0xFFFF;
330 uint16_t ir = combined_illuminance >> 16;
331 return (ir > full) ? 0 : (full - ir);
332 }
333 // unknown channel!
334 ESP_LOGE(TAG, "get_illuminance() caller requested an unknown channel: %d", channel);
335 return 0;
336}
337
352float TSL2591Component::get_calculated_lux(uint16_t full_spectrum, uint16_t infrared) {
353 // Check for overflow conditions first
354 uint16_t max_count = (this->integration_time_ == TSL2591_INTEGRATION_TIME_100MS ? 36863 : 65535);
355 if ((full_spectrum == max_count) || (infrared == max_count)) {
356 // Signal an overflow
357 ESP_LOGW(TAG, "Apparent saturation on '%s'; try reducing the gain or integration time", this->name_);
358 return NAN;
359 }
360
361 if ((full_spectrum == 0) && (infrared == 0)) {
362 // trivial conversion; avoids divide by 0
363 ESP_LOGW(TAG, "Zero reading on both '%s' sensors", this->name_);
364 return 0.0F;
365 }
366
367 float atime = 100.F + (this->integration_time_ * 100);
368
369 float again;
370 switch (this->gain_) {
371 case TSL2591_GAIN_LOW:
372 again = 1.0F;
373 break;
374 case TSL2591_GAIN_MED:
375 again = 25.0F;
376 break;
378 again = 400.0F;
379 break;
380 case TSL2591_GAIN_MAX:
381 again = 9500.0F;
382 break;
383 default:
384 again = 1.0F;
385 break;
386 }
387 // This lux equation is copied from the Adafruit TSL2591 v1.4.0 and modified slightly.
388 // See: https://github.com/adafruit/Adafruit_TSL2591_Library/issues/14
389 // and that library code.
390 // They said:
391 // Note: This algorithm is based on preliminary coefficients
392 // provided by AMS and may need to be updated in the future
393 // However, we use gain multipliers that are more in line with the midpoints
394 // of ranges from the datasheet. We don't know why the other libraries
395 // used the values they did for HIGH and MAX.
396 // If cpl or full_spectrum are 0, this will return NaN due to divide by 0.
397 // For the curious "cpl" is counts per lux, a term used in AMS application notes.
398 float cpl = (atime * again) / (this->device_factor_ * this->glass_attenuation_factor_);
399 float lux = (((float) full_spectrum - (float) infrared)) * (1.0F - ((float) infrared / (float) full_spectrum)) / cpl;
400 return std::max(lux, 0.0F);
401}
402
416void TSL2591Component::automatic_gain_update(uint16_t full_spectrum) {
417 TSL2591Gain new_gain = this->gain_;
418 uint fs_divider = (this->integration_time_ == TSL2591_INTEGRATION_TIME_100MS) ? 2 : 1;
419
420 switch (this->gain_) {
421 case TSL2591_GAIN_LOW:
422 if (full_spectrum < 54) { // 1/3 FS / GAIN_HIGH
423 new_gain = TSL2591_GAIN_HIGH;
424 } else if (full_spectrum < 875) { // 1/3 FS / GAIN_MED
425 new_gain = TSL2591_GAIN_MED;
426 }
427 break;
428 case TSL2591_GAIN_MED:
429 if (full_spectrum < 57) { // 1/3 FS / (GAIN_MAX/GAIN_MED)
430 new_gain = TSL2591_GAIN_MAX;
431 } else if (full_spectrum < 1365) { // 1/3 FS / (GAIN_HIGH/GAIN_MED)
432 new_gain = TSL2591_GAIN_HIGH;
433 } else if (full_spectrum > 62000 / fs_divider) { // 2/3 FS / (GAIN_LOW/GAIN_MED) clipped to 95% FS
434 new_gain = TSL2591_GAIN_LOW;
435 }
436 break;
438 if (full_spectrum < 920) { // 1/3 FS / (GAIN_MAX/GAIN_HIGH)
439 new_gain = TSL2591_GAIN_MAX;
440 } else if (full_spectrum > 62000 / fs_divider) { // 2/3 FS / (GAIN_MED/GAIN_HIGH) clipped to 95% FS
441 new_gain = TSL2591_GAIN_LOW;
442 }
443 break;
444 case TSL2591_GAIN_MAX:
445 if (full_spectrum > 62000 / fs_divider) // 2/3 FS / (GAIN_MED/GAIN_HIGH) clipped to 95% FS
446 new_gain = TSL2591_GAIN_MED;
447 break;
448 }
449
450 if (this->gain_ != new_gain) {
451 this->gain_ = new_gain;
453 }
454 ESP_LOGD(TAG, "Gain setting: %d", this->gain_);
455}
456
462 switch (this->gain_) {
463 case TSL2591_GAIN_LOW:
464 return 1.0F;
465 case TSL2591_GAIN_MED:
466 return 25.0F;
468 return 400.0F;
469 case TSL2591_GAIN_MAX:
470 return 9500.0F;
471 default:
472 // Shouldn't get here, but just in case.
473 return NAN;
474 }
475}
476
477} // namespace esphome::tsl2591
uint8_t status
Definition bl0942.h:8
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:272
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_interval(const std voi set_interval)(const char *name, uint32_t interval, std::function< void()> &&f)
Set an interval function with a unique name.
Definition component.h:400
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_interval(const std boo cancel_interval)(const char *name)
Cancel an interval function.
Definition component.h:422
void status_clear_warning()
Definition component.h:289
bool write_byte(uint8_t a_register, uint8_t data) const
Definition i2c.h:265
bool read_byte(uint8_t a_register, uint8_t *data)
Definition i2c.h:240
Base-class for all sensors.
Definition sensor.h:47
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:68
TSL2591IntegrationTime integration_time_
Definition tsl2591.h:259
void dump_config() override
Used by ESPHome framework.
Definition tsl2591.cpp:80
TSL2591ComponentGain component_gain_
Definition tsl2591.h:260
void automatic_gain_update(uint16_t full_spectrum)
Updates the gain setting based on the most recent full spectrum reading.
Definition tsl2591.cpp:416
bool is_adc_valid()
Are the device ADC values valid?
Definition tsl2591.cpp:249
sensor::Sensor * infrared_sensor_
Definition tsl2591.h:255
float get_calculated_lux(uint16_t full_spectrum, uint16_t infrared)
Calculates and returns a lux value based on the ADC readings.
Definition tsl2591.cpp:352
sensor::Sensor * calculated_lux_sensor_
Definition tsl2591.h:257
void set_visible_sensor(sensor::Sensor *visible_sensor)
Used by ESPHome framework.
Definition tsl2591.cpp:203
void set_device_and_glass_attenuation_factors(float device_factor, float glass_attenuation_factor)
Sets the device and glass attenuation factors.
Definition tsl2591.cpp:223
sensor::Sensor * full_spectrum_sensor_
Definition tsl2591.h:254
void set_integration_time(TSL2591IntegrationTime integration_time)
Used by ESPHome framework.
Definition tsl2591.cpp:217
void set_calculated_lux_sensor(sensor::Sensor *calculated_lux_sensor)
Used by ESPHome framework.
Definition tsl2591.cpp:209
uint32_t get_combined_illuminance()
Get the combined illuminance value.
Definition tsl2591.cpp:258
void set_name(const char *name)
Sets the name for this instance of the device.
Definition tsl2591.cpp:247
sensor::Sensor * visible_sensor_
Definition tsl2591.h:256
void set_infrared_sensor(sensor::Sensor *infrared_sensor)
Used by ESPHome framework.
Definition tsl2591.cpp:199
sensor::Sensor * actual_gain_sensor_
Definition tsl2591.h:258
void set_actual_gain_sensor(sensor::Sensor *actual_gain_sensor)
Used by ESPHome framework.
Definition tsl2591.cpp:213
void set_gain(TSL2591ComponentGain gain)
Used by ESPHome framework.
Definition tsl2591.cpp:221
void enable()
Powers on the TSL2591 device and enables its sensors.
Definition tsl2591.cpp:25
void update() override
Used by ESPHome framework.
Definition tsl2591.cpp:182
void set_integration_time_and_gain(TSL2591IntegrationTime integration_time, TSL2591Gain gain)
Set device integration time and gain.
Definition tsl2591.cpp:228
void set_full_spectrum_sensor(sensor::Sensor *full_spectrum_sensor)
Used by ESPHome framework.
Definition tsl2591.cpp:205
void disable()
Powers off the TSL2591 device.
Definition tsl2591.cpp:32
float get_actual_gain()
Reads the actual gain used.
Definition tsl2591.cpp:461
uint16_t get_illuminance(TSL2591SensorChannel channel)
Get an individual sensor channel reading.
Definition tsl2591.cpp:315
void setup() override
Used by ESPHome framework.
Definition tsl2591.cpp:44
void set_power_save_mode(bool enable)
Should the device be powered down between readings?
Definition tsl2591.cpp:245
uint16_t id
AlsGain501 gain
IntegrationTime501 integration_time
TSL2591Gain
Enum listing all gain settings for the TSL2591.
Definition tsl2591.h:43
TSL2591ComponentGain
Enum listing all gain settings for the TSL2591 component.
Definition tsl2591.h:30
TSL2591IntegrationTime
Enum listing all conversion/integration time settings for the TSL2591.
Definition tsl2591.h:16
@ TSL2591_INTEGRATION_TIME_100MS
Definition tsl2591.h:17
TSL2591SensorChannel
Enum listing sensor channels.
Definition tsl2591.h:54
@ TSL2591_SENSOR_CHANNEL_INFRARED
Definition tsl2591.h:56
@ TSL2591_SENSOR_CHANNEL_VISIBLE
Definition tsl2591.h:55
@ TSL2591_SENSOR_CHANNEL_FULL_SPECTRUM
Definition tsl2591.h:57
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
static void uint32_t