diff --git a/CODEOWNERS b/CODEOWNERS index b5603ea1c0..f45c9c1db4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -138,6 +138,7 @@ esphome/components/tmp102/* @timsavage esphome/components/tmp117/* @Azimath esphome/components/tof10120/* @wstrzalka esphome/components/toshiba/* @kbx81 +esphome/components/tsl2591/* @wjcarpenter esphome/components/tuya/binary_sensor/* @jesserockz esphome/components/tuya/climate/* @jesserockz esphome/components/tuya/sensor/* @jesserockz diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py index 3bf828aafa..ce567bc3f1 100644 --- a/esphome/components/nextion/base_component.py +++ b/esphome/components/nextion/base_component.py @@ -2,7 +2,9 @@ from string import ascii_letters, digits import esphome.config_validation as cv import esphome.codegen as cg from esphome.components import color - +from esphome.const import ( + CONF_VISIBLE, +) from . import CONF_NEXTION_ID from . import Nextion @@ -25,7 +27,6 @@ CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color" CONF_FOREGROUND_COLOR = "foreground_color" CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color" CONF_FONT_ID = "font_id" -CONF_VISIBLE = "visible" def NextionName(value): diff --git a/esphome/components/tsl2591/__init__.py b/esphome/components/tsl2591/__init__.py new file mode 100644 index 0000000000..63331641c5 --- /dev/null +++ b/esphome/components/tsl2591/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@wjcarpenter"] diff --git a/esphome/components/tsl2591/sensor.py b/esphome/components/tsl2591/sensor.py new file mode 100644 index 0000000000..095a8c886c --- /dev/null +++ b/esphome/components/tsl2591/sensor.py @@ -0,0 +1,168 @@ +# Credit where due.... +# I put a certain amount of work into this, but a lot of ESPHome integration is +# "look for other examples and see what they do" programming-by-example. Here are +# things that helped me along with this: +# +# - I mined the existing tsl2561 integration for basic structural framing for both +# the code and documentation. +# +# - I looked at the existing bme280 integration as an example of a single device +# with multiple sensors. +# +# - Comments and code in this thread got me going with the Adafruit TSL2591 library +# and prompted my desired to have tsl2591 as a standard component instead of a +# custom/external component. +# +# - And, of course, the handy and available Adafruit TSL2591 library was very +# helpful in understanding what the device is actually talking about. +# +# Here is the project that started me down the TSL2591 device trail in the first +# place: https://hackaday.io/project/176690-the-water-watcher + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_GAIN, + CONF_ID, + CONF_NAME, + CONF_INTEGRATION_TIME, + CONF_FULL_SPECTRUM, + CONF_INFRARED, + CONF_POWER_SAVE_MODE, + CONF_VISIBLE, + CONF_CALCULATED_LUX, + CONF_DEVICE_FACTOR, + CONF_GLASS_ATTENUATION_FACTOR, + ICON_BRIGHTNESS_6, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_ILLUMINANCE, + STATE_CLASS_MEASUREMENT, + UNIT_EMPTY, + UNIT_LUX, +) + +DEPENDENCIES = ["i2c"] + +tsl2591_ns = cg.esphome_ns.namespace("tsl2591") + +TSL2591IntegrationTime = tsl2591_ns.enum("TSL2591IntegrationTime") +INTEGRATION_TIMES = { + 100: TSL2591IntegrationTime.TSL2591_INTEGRATION_TIME_100MS, + 200: TSL2591IntegrationTime.TSL2591_INTEGRATION_TIME_200MS, + 300: TSL2591IntegrationTime.TSL2591_INTEGRATION_TIME_300MS, + 400: TSL2591IntegrationTime.TSL2591_INTEGRATION_TIME_400MS, + 500: TSL2591IntegrationTime.TSL2591_INTEGRATION_TIME_500MS, + 600: TSL2591IntegrationTime.TSL2591_INTEGRATION_TIME_600MS, +} + +TSL2591Gain = tsl2591_ns.enum("TSL2591Gain") +GAINS = { + "1X": TSL2591Gain.TSL2591_GAIN_LOW, + "LOW": TSL2591Gain.TSL2591_GAIN_LOW, + "25X": TSL2591Gain.TSL2591_GAIN_MED, + "MED": TSL2591Gain.TSL2591_GAIN_MED, + "MEDIUM": TSL2591Gain.TSL2591_GAIN_MED, + "400X": TSL2591Gain.TSL2591_GAIN_HIGH, + "HIGH": TSL2591Gain.TSL2591_GAIN_HIGH, + "9500X": TSL2591Gain.TSL2591_GAIN_MAX, + "MAX": TSL2591Gain.TSL2591_GAIN_MAX, + "MAXIMUM": TSL2591Gain.TSL2591_GAIN_MAX, +} + + +def validate_integration_time(value): + value = cv.positive_time_period_milliseconds(value).total_milliseconds + return cv.enum(INTEGRATION_TIMES, int=True)(value) + + +TSL2591Component = tsl2591_ns.class_( + "TSL2591Component", cg.PollingComponent, i2c.I2CDevice +) + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(TSL2591Component), + cv.Optional(CONF_INFRARED): sensor.sensor_schema( + UNIT_EMPTY, + ICON_BRIGHTNESS_6, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_VISIBLE): sensor.sensor_schema( + UNIT_EMPTY, + ICON_BRIGHTNESS_6, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_FULL_SPECTRUM): sensor.sensor_schema( + UNIT_EMPTY, + ICON_BRIGHTNESS_6, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CALCULATED_LUX): sensor.sensor_schema( + UNIT_LUX, + ICON_BRIGHTNESS_6, + 4, + DEVICE_CLASS_ILLUMINANCE, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + CONF_INTEGRATION_TIME, default="100ms" + ): validate_integration_time, + cv.Optional(CONF_NAME, default="TLS2591"): cv.string, + cv.Optional(CONF_GAIN, default="MEDIUM"): cv.enum(GAINS, upper=True), + cv.Optional(CONF_POWER_SAVE_MODE, default=True): cv.boolean, + cv.Optional(CONF_DEVICE_FACTOR, default=53.0): cv.float_with_unit( + "device_factor", "", True + ), + cv.Optional(CONF_GLASS_ATTENUATION_FACTOR, default=7.7): cv.float_with_unit( + "glass_attenuation_factor", "", True + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x29)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_FULL_SPECTRUM in config: + conf = config[CONF_FULL_SPECTRUM] + sens = await sensor.new_sensor(conf) + cg.add(var.set_full_spectrum_sensor(sens)) + + if CONF_INFRARED in config: + conf = config[CONF_INFRARED] + sens = await sensor.new_sensor(conf) + cg.add(var.set_infrared_sensor(sens)) + + if CONF_VISIBLE in config: + conf = config[CONF_VISIBLE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_visible_sensor(sens)) + + if CONF_CALCULATED_LUX in config: + conf = config[CONF_CALCULATED_LUX] + sens = await sensor.new_sensor(conf) + cg.add(var.set_calculated_lux_sensor(sens)) + + cg.add(var.set_name(config[CONF_NAME])) + cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) + cg.add(var.set_integration_time(config[CONF_INTEGRATION_TIME])) + cg.add(var.set_gain(config[CONF_GAIN])) + cg.add( + var.set_device_and_glass_attenuation_factors( + config[CONF_DEVICE_FACTOR], config[CONF_GLASS_ATTENUATION_FACTOR] + ) + ) diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp new file mode 100644 index 0000000000..1785fa46b4 --- /dev/null +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -0,0 +1,369 @@ +#include "tsl2591.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tsl2591 { + +static const char *const TAG = "tsl2591.sensor"; + +// Various constants used in TSL2591 register manipulation +#define TSL2591_COMMAND_BIT (0xA0) // 1010 0000: bits 7 and 5 for 'command, normal' +#define TSL2591_ENABLE_POWERON (0x01) // Flag for ENABLE register, to enable +#define TSL2591_ENABLE_POWEROFF (0x00) // Flag for ENABLE register, to disable +#define TSL2591_ENABLE_AEN (0x02) // Flag for ENABLE register, to turn on ADCs + +// TSL2591 registers from the datasheet. We only define what we use. +#define TSL2591_REGISTER_ENABLE (0x00) +#define TSL2591_REGISTER_CONTROL (0x01) +#define TSL2591_REGISTER_DEVICE_ID (0x12) +#define TSL2591_REGISTER_STATUS (0x13) +#define TSL2591_REGISTER_CHAN0_LOW (0x14) +#define TSL2591_REGISTER_CHAN0_HIGH (0x15) +#define TSL2591_REGISTER_CHAN1_LOW (0x16) +#define TSL2591_REGISTER_CHAN1_HIGH (0x17) + +void TSL2591Component::enable() { + // Enable the device by setting the control bit to 0x01. Also turn on ADCs. + if (!this->write_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_ENABLE, TSL2591_ENABLE_POWERON | TSL2591_ENABLE_AEN)) { + ESP_LOGE(TAG, "Failed I2C write during enable()"); + } +} + +void TSL2591Component::disable() { + if (!this->write_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_ENABLE, TSL2591_ENABLE_POWEROFF)) { + ESP_LOGE(TAG, "Failed I2C write during disable()"); + } +} + +void TSL2591Component::disable_if_power_saving_() { + if (this->power_save_mode_enabled_) { + this->disable(); + } +} + +void TSL2591Component::setup() { + uint8_t address = this->address_; + ESP_LOGI(TAG, "Setting up TSL2591 sensor at I2C address 0x%02X", address); + uint8_t id; + if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_DEVICE_ID, &id)) { + ESP_LOGE(TAG, "Failed I2C read during setup()"); + this->mark_failed(); + return; + } + if (id != 0x50) { + ESP_LOGE(TAG, + "Could not find the TSL2591 sensor. The ID register of the device at address 0x%02X reported 0x%02X " + "instead of 0x50.", + address, id); + this->mark_failed(); + return; + } + this->set_integration_time_and_gain(this->integration_time_, this->gain_); + this->disable_if_power_saving_(); +} + +void TSL2591Component::dump_config() { + ESP_LOGCONFIG(TAG, "TSL2591:"); + LOG_I2C_DEVICE(this); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with TSL2591 failed earlier, during setup"); + return; + } + + ESP_LOGCONFIG(TAG, " Name: %s", this->name_); + TSL2591Gain raw_gain = this->gain_; + int gain = 0; + std::string gain_word = "unknown"; + switch (raw_gain) { + case TSL2591_GAIN_LOW: + gain = 1; + gain_word = "low"; + break; + case TSL2591_GAIN_MED: + gain = 25; + gain_word = "medium"; + break; + case TSL2591_GAIN_HIGH: + gain = 400; + gain_word = "high"; + break; + case TSL2591_GAIN_MAX: + gain = 9500; + gain_word = "maximum"; + break; + } + ESP_LOGCONFIG(TAG, " Gain: %dx (%s)", gain, gain_word.c_str()); + TSL2591IntegrationTime raw_timing = this->integration_time_; + int timing_ms = (1 + raw_timing) * 100; + ESP_LOGCONFIG(TAG, " Integration Time: %d ms", timing_ms); + ESP_LOGCONFIG(TAG, " Power save mode enabled: %s", ONOFF(this->power_save_mode_enabled_)); + ESP_LOGCONFIG(TAG, " Device factor: %f", this->device_factor_); + ESP_LOGCONFIG(TAG, " Glass attenuation factor: %f", this->glass_attenuation_factor_); + LOG_SENSOR(" ", "Full spectrum:", this->full_spectrum_sensor_); + LOG_SENSOR(" ", "Infrared:", this->infrared_sensor_); + LOG_SENSOR(" ", "Visible:", this->visible_sensor_); + LOG_SENSOR(" ", "Calculated lux:", this->calculated_lux_sensor_); + + LOG_UPDATE_INTERVAL(this); +} + +void TSL2591Component::process_update_() { + uint32_t combined = this->get_combined_illuminance(); + uint16_t visible = this->get_illuminance(TSL2591_SENSOR_CHANNEL_VISIBLE, combined); + uint16_t infrared = this->get_illuminance(TSL2591_SENSOR_CHANNEL_INFRARED, combined); + uint16_t full = this->get_illuminance(TSL2591_SENSOR_CHANNEL_FULL_SPECTRUM, combined); + float lux = this->get_calculated_lux(full, infrared); + ESP_LOGD(TAG, "Got illuminance: combined 0x%X, full %d, IR %d, vis %d. Calc lux: %f", combined, full, infrared, + visible, lux); + if (this->full_spectrum_sensor_ != nullptr) { + this->full_spectrum_sensor_->publish_state(full); + } + if (this->infrared_sensor_ != nullptr) { + this->infrared_sensor_->publish_state(infrared); + } + if (this->visible_sensor_ != nullptr) { + this->visible_sensor_->publish_state(visible); + } + if (this->calculated_lux_sensor_ != nullptr) { + this->calculated_lux_sensor_->publish_state(lux); + } + this->status_clear_warning(); +} + +#define interval_name "tsl2591_interval_for_update" + +void TSL2591Component::interval_function_for_update_() { + if (!this->is_adc_valid()) { + uint64_t now = millis(); + ESP_LOGD(TAG, "Elapsed %3llu ms; still waiting for valid ADC", (now - this->interval_start_)); + if (now > this->interval_timeout_) { + ESP_LOGW(TAG, "Interval timeout for TSL2591 '%s' expired before ADCs became valid.", this->name_); + this->cancel_interval(interval_name); + } + return; + } + this->cancel_interval(interval_name); + this->process_update_(); +} + +void TSL2591Component::update() { + if (!is_failed()) { + if (this->power_save_mode_enabled_) { + // we enabled it here, else ADC will never become valid + // but actually doing the reads will disable device if needed + this->enable(); + } + if (this->is_adc_valid()) { + this->process_update_(); + } else { + this->interval_start_ = millis(); + this->interval_timeout_ = this->interval_start_ + 620; + this->set_interval(interval_name, 100, [this] { this->interval_function_for_update_(); }); + } + } +} + +void TSL2591Component::set_infrared_sensor(sensor::Sensor *infrared_sensor) { + this->infrared_sensor_ = infrared_sensor; +} + +void TSL2591Component::set_visible_sensor(sensor::Sensor *visible_sensor) { this->visible_sensor_ = visible_sensor; } + +void TSL2591Component::set_full_spectrum_sensor(sensor::Sensor *full_spectrum_sensor) { + this->full_spectrum_sensor_ = full_spectrum_sensor; +} + +void TSL2591Component::set_calculated_lux_sensor(sensor::Sensor *calculated_lux_sensor) { + this->calculated_lux_sensor_ = calculated_lux_sensor; +} + +void TSL2591Component::set_integration_time(TSL2591IntegrationTime integration_time) { + this->integration_time_ = integration_time; +} + +void TSL2591Component::set_gain(TSL2591Gain gain) { this->gain_ = gain; } + +void TSL2591Component::set_device_and_glass_attenuation_factors(float device_factor, float glass_attenuation_factor) { + this->device_factor_ = device_factor; + this->glass_attenuation_factor_ = glass_attenuation_factor; +} + +void TSL2591Component::set_integration_time_and_gain(TSL2591IntegrationTime integration_time, TSL2591Gain gain) { + this->enable(); + this->integration_time_ = integration_time; + this->gain_ = gain; + if (!this->write_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CONTROL, + this->integration_time_ | this->gain_)) { // NOLINT + ESP_LOGE(TAG, "Failed I2C write during set_integration_time_and_gain()"); + } + // The ADC values can be confused if gain or integration time are changed in the middle of a cycle. + // So, we unconditionally disable the device to turn the ADCs off. When re-enabling, the ADCs + // will tell us when they are ready again. That avoids an initial bogus reading. + this->disable(); + if (!this->power_save_mode_enabled_) { + this->enable(); + } +} + +void TSL2591Component::set_power_save_mode(bool enable) { this->power_save_mode_enabled_ = enable; } + +void TSL2591Component::set_name(const char *name) { this->name_ = name; } + +float TSL2591Component::get_setup_priority() const { return setup_priority::DATA; } + +bool TSL2591Component::is_adc_valid() { + uint8_t status; + if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_STATUS, &status)) { + ESP_LOGE(TAG, "Failed I2C read during is_adc_valid()"); + return false; + } + return status & 0x01; +} + +uint32_t TSL2591Component::get_combined_illuminance() { + this->enable(); + // Wait x ms for ADC to complete and signal valid. + // The max integration time is 600ms, so that's our max delay. + // (But we use 620ms as a bit of slack.) + // We'll do mini-delays and break out as soon as the ADC is good. + bool avalid; + const uint8_t mini_delay = 100; + for (uint16_t d = 0; d < 620; d += mini_delay) { + avalid = this->is_adc_valid(); + if (avalid) { + break; + } + // we only log this if we need any delay, since normally we don't + ESP_LOGD(TAG, " after %3d ms: ADC valid? %s", d, avalid ? "true" : "false"); + delay(mini_delay); + } + if (!avalid) { + // still not valid after a sutiable delay + // we don't mark the device as failed since it might come around in the future (probably not :-() + ESP_LOGE(TAG, "tsl2591 device '%s' did not return valid readings.", this->name_); + this->disable_if_power_saving_(); + return 0; + } + + // CHAN0 must be read before CHAN1 + // See: https://forums.adafruit.com/viewtopic.php?f=19&t=124176 + // Also, low byte must be read before high byte.. + // We read the registers in the order described in the datasheet. + uint32_t x32; + uint8_t ch0low, ch0high, ch1low, ch1high; + uint16_t ch0_16; + uint16_t ch1_16; + if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CHAN0_LOW, &ch0low)) { + ESP_LOGE(TAG, "Failed I2C read during get_combined_illuminance()"); + return 0; + } + if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CHAN0_HIGH, &ch0high)) { + ESP_LOGE(TAG, "Failed I2C read during get_combined_illuminance()"); + return 0; + } + ch0_16 = (ch0high << 8) | ch0low; + if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CHAN1_LOW, &ch1low)) { + ESP_LOGE(TAG, "Failed I2C read during get_combined_illuminance()"); + return 0; + } + if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CHAN1_HIGH, &ch1high)) { + ESP_LOGE(TAG, "Failed I2C read during get_combined_illuminance()"); + return 0; + } + ch1_16 = (ch1high << 8) | ch1low; + x32 = (ch1_16 << 16) | ch0_16; + + this->disable_if_power_saving_(); + return x32; +} + +uint16_t TSL2591Component::get_illuminance(TSL2591SensorChannel channel) { + uint32_t combined = this->get_combined_illuminance(); + return this->get_illuminance(channel, combined); +} +// logic cloned from Adafruit TSL2591 library +uint16_t TSL2591Component::get_illuminance(TSL2591SensorChannel channel, uint32_t combined_illuminance) { + if (channel == TSL2591_SENSOR_CHANNEL_FULL_SPECTRUM) { + // Reads two byte value from channel 0 (visible + infrared) + return (combined_illuminance & 0xFFFF); + } else if (channel == TSL2591_SENSOR_CHANNEL_INFRARED) { + // Reads two byte value from channel 1 (infrared) + return (combined_illuminance >> 16); + } else if (channel == TSL2591_SENSOR_CHANNEL_VISIBLE) { + // Reads all and subtracts out the infrared + return ((combined_illuminance & 0xFFFF) - (combined_illuminance >> 16)); + } + // unknown channel! + ESP_LOGE(TAG, "TSL2591Component::get_illuminance() caller requested an unknown channel: %d", channel); + return 0; +} + +/** Calculates a lux value from the two TSL2591 physical sensor ADC readings. + * + * The lux calculation is copied from the Adafruit TSL2591 library. + * There is some debate about whether it is the correct lux equation to use. + * We use that lux equation because (a) it helps with a transition from + * using that Adafruit library to using this ESPHome integration, and (b) we + * don't have a definitive better idea. + * + * Since the raw ADC readings are available, you can ignore this method and + * implement your own lux equation. + * + * @param full_spectrum The ADC reading for TSL2591 channel 0. + * @param infrared The ADC reading for TSL2591 channel 1. + */ +float TSL2591Component::get_calculated_lux(uint16_t full_spectrum, uint16_t infrared) { + // Check for overflow conditions first + uint16_t max_count = (this->integration_time_ == TSL2591_INTEGRATION_TIME_100MS ? 36863 : 65535); + if ((full_spectrum == max_count) || (infrared == max_count)) { + // Signal an overflow + ESP_LOGW(TAG, "Apparent saturation on TSL2591 (%s). You could reduce the gain.", this->name_); + return -1.0F; + } + + if ((full_spectrum == 0) && (infrared == 0)) { + // trivial conversion; avoids divide by 0 + ESP_LOGW(TAG, "Zero reading on both TSL2591 (%s) sensors. Is the device having a problem?", this->name_); + return 0.0F; + } + + float atime = 100.F + (this->integration_time_ * 100); + + float again; + switch (this->gain_) { + case TSL2591_GAIN_LOW: + again = 1.0F; + break; + case TSL2591_GAIN_MED: + again = 25.0F; + break; + case TSL2591_GAIN_HIGH: + again = 400.0F; + break; + case TSL2591_GAIN_MAX: + again = 9500.0F; + break; + default: + again = 1.0F; + break; + } + + // This lux equation is copied from the Adafruit TSL2591 v1.4.0 and modified slightly. + // See: https://github.com/adafruit/Adafruit_TSL2591_Library/issues/14 + // and that library code. + // They said: + // Note: This algorithm is based on preliminary coefficients + // provided by AMS and may need to be updated in the future + // However, we use gain multipliers that are more in line with the midpoints + // of ranges from the datasheet. We don't know why the other libraries + // used the values they did for HIGH and MAX. + // If cpl or full_spectrum are 0, this will return NaN due to divide by 0. + // For the curious "cpl" is counts per lux, a term used in AMS application notes. + float cpl = (atime * again) / (this->device_factor_ * this->glass_attenuation_factor_); + float lux = (((float) full_spectrum - (float) infrared)) * (1.0F - ((float) infrared / (float) full_spectrum)) / cpl; + return max(lux, 0.0F); +} + +} // namespace tsl2591 +} // namespace esphome diff --git a/esphome/components/tsl2591/tsl2591.h b/esphome/components/tsl2591/tsl2591.h new file mode 100644 index 0000000000..d377d082a8 --- /dev/null +++ b/esphome/components/tsl2591/tsl2591.h @@ -0,0 +1,245 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace tsl2591 { + +/** Enum listing all conversion/integration time settings for the TSL2591. + * + * Specific values of the enum constants are register values taken from the TSL2591 datasheet. + * Longer times mean more accurate results, but will take more energy/more time. + */ +enum TSL2591IntegrationTime { + TSL2591_INTEGRATION_TIME_100MS = 0b000, + TSL2591_INTEGRATION_TIME_200MS = 0b001, + TSL2591_INTEGRATION_TIME_300MS = 0b010, + TSL2591_INTEGRATION_TIME_400MS = 0b011, + TSL2591_INTEGRATION_TIME_500MS = 0b100, + TSL2591_INTEGRATION_TIME_600MS = 0b101, +}; + +/** Enum listing all gain settings for the TSL2591. + * + * Specific values of the enum constants are register values taken from the TSL2591 datasheet. + * Higher values are better for low light situations, but can increase noise. + */ +enum TSL2591Gain { + TSL2591_GAIN_LOW = 0b00 << 4, // 1x + TSL2591_GAIN_MED = 0b01 << 4, // 25x + TSL2591_GAIN_HIGH = 0b10 << 4, // 400x + TSL2591_GAIN_MAX = 0b11 << 4, // 9500x +}; + +/** Enum listing sensor channels. + * + * They identify the type of light to report. + */ +enum TSL2591SensorChannel { + TSL2591_SENSOR_CHANNEL_VISIBLE, + TSL2591_SENSOR_CHANNEL_INFRARED, + TSL2591_SENSOR_CHANNEL_FULL_SPECTRUM, +}; + +/// This class includes support for the TSL2591 i2c ambient light +/// sensor. The device has two distinct sensors. One is for visible +/// light plus infrared light, and the other is for infrared +/// light. They are reported as separate sensors, and the difference +/// between the values is reported as a third sensor as a convenience +/// for visible light only. +class TSL2591Component : public PollingComponent, public i2c::I2CDevice { + public: + /** Set device integration time and gain. + * + * These are set as a single I2C transaction, so you must supply values + * for both. + * + * Longer integration times provides more accurate values, but also + * means more power consumption. Higher gain values are useful for + * lower light intensities but are also subject to more noise. The + * device might use a slightly different gain multiplier than those + * indicated; see the datasheet for details. + * + * Possible values for integration_time (from enum + * TSL2591IntegrationTime) are: + * + * - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_100MS` + * - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_200MS` + * - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_300MS` + * - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_400MS` + * - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_500MS` + * - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_600MS` + * + * Possible values for gain (from enum TSL2591Gain) are: + * + * - `esphome::tsl2591::TSL2591_GAIN_LOW` (1x) + * - `esphome::tsl2591::TSL2591_GAIN_MED` (25x) + * - `esphome::tsl2591::TSL2591_GAIN_HIGH` (400x) + * - `esphome::tsl2591::TSL2591_GAIN_MAX` (9500x) + * + * @param integration_time The new integration time. + * @param gain The new gain. + */ + void set_integration_time_and_gain(TSL2591IntegrationTime integration_time, TSL2591Gain gain); + + /** Should the device be powered down between readings? + * + * The disadvantage of powering down the device between readings + * is that you have to wait for the ADC to go through an + * integration cycle before a reliable reading is available. + * This happens during ESPHome's update loop, so waiting slows + * down the entire ESP device. You should only enable this if + * you need to minimize power consumption and you can tolerate + * that delay. Otherwise, keep the default of disabling + * power save mode. + * + * @param enable Enable or disable power save mode. + */ + void set_power_save_mode(bool enable); + + /** Sets the name for this instance of the device. + * + * @param name The user-friendly name. + */ + void set_name(const char *name); + + /** Sets the device and glass attenuation factors. + * + * The lux equation, used to calculate the lux from the ADC readings, + * involves a scaling coefficient that is the product of a device + * factor (specific to the type of device being used) and a glass + * attenuation factor (specific to whatever glass or plastic cover + * is installed in front of the light sensors. + * + * AMS does not publish the device factor for the TSL2591. In the + * datasheet for the earlier TSL2571 and in application notes, they + * use the value 53, so we use that as the default. + * + * The glass attenuation factor depends on factors external to the + * TSL2591 and is best obtained through experimental measurements. + * The Adafruit TSL2591 library use a value of ~7.7, which we use as + * a default. Waveshare uses a value of ~14.4. Presumably, those + * factors are appropriate to the breakout boards from those vendors, + * but we have not verified that. + * + * @param device_factor The device factor. + * @param glass_attenuation_factor The glass attenuation factor. + */ + void set_device_and_glass_attenuation_factors(float device_factor, float glass_attenuation_factor); + + /** Calculates and returns a lux value based on the ADC readings. + * + * @param full_spectrum The ADC reading for the full spectrum sensor. + * @param infrared The ADC reading for the infrared sensor. + */ + float get_calculated_lux(uint16_t full_spectrum, uint16_t infrared); + + /** Get the combined illuminance value. + * + * This is encoded into a 32 bit value. The high 16 bits are the value of the + * infrared sensor. The low 16 bits are the sum of the combined sensor values. + * + * If power saving mode is enabled, there can be a delay (up to the value of the integration + * time) while waiting for the device ADCs to signal that values are valid. + */ + uint32_t get_combined_illuminance(); + + /** Get an individual sensor channel reading. + * + * This gets an individual light sensor reading. Since it goes through + * the entire component read cycle to get one value, it's not optimal if + * you want to get all possible channel values. If you want that, first + * call `get_combined_illuminance()` and pass that value to the companion + * method with a different signature. + * + * If power saving mode is enabled, there can be a delay (up to the value of the integration + * time) while waiting for the device ADCs to signal that values are valid. + * + * @param channel The sensor channel of interest. + */ + uint16_t get_illuminance(TSL2591SensorChannel channel); + + /** Get an individual sensor channel reading from combined illuminance. + * + * This gets an individual light sensor reading from a combined illuminance + * value, which you would obtain from calling `getCombinedIlluminance()`. + * This method does not communicate with the sensor at all. It's strictly + * local calculations, so it is efficient if you call it multiple times. + * + * @param channel The sensor channel of interest. + * @param combined_illuminance The previously obtained combined illuminance value. + */ + uint16_t get_illuminance(TSL2591SensorChannel channel, uint32_t combined_illuminance); + + /** Are the device ADC values valid? + * + * Useful for scripting. This should be checked before calling update(). + * It asks the TSL2591 if the ADC has completed an integration cycle + * and has reliable values in the device registers. If you call update() + * before the ADC values are valid, you may cause a general delay in + * the ESPHome update loop. + * + * It should take no more than the configured integration time for + * the ADC values to become valid after the TSL2591 device is enabled. + */ + bool is_adc_valid(); + + /** Powers on the TSL2591 device and enables its sensors. + * + * You only need to call this if you have disabled the device. + * The device starts enabled in ESPHome unless power save mode is enabled. + */ + void enable(); + /** Powers off the TSL2591 device. + * + * You can call this from an ESPHome script if you are explicitly + * controlling TSL2591 power consumption. + * The device starts enabled in ESPHome unless power save mode is enabled. + */ + void disable(); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these. They're for ESPHome integration use.) + /** Used by ESPHome framework. */ + void set_full_spectrum_sensor(sensor::Sensor *full_spectrum_sensor); + /** Used by ESPHome framework. */ + void set_infrared_sensor(sensor::Sensor *infrared_sensor); + /** Used by ESPHome framework. */ + void set_visible_sensor(sensor::Sensor *visible_sensor); + /** Used by ESPHome framework. */ + void set_calculated_lux_sensor(sensor::Sensor *calculated_lux_sensor); + /** Used by ESPHome framework. Does NOT actually set the value on the device. */ + void set_integration_time(TSL2591IntegrationTime integration_time); + /** Used by ESPHome framework. Does NOT actually set the value on the device. */ + void set_gain(TSL2591Gain gain); + /** Used by ESPHome framework. */ + void setup() override; + /** Used by ESPHome framework. */ + void dump_config() override; + /** Used by ESPHome framework. */ + void update() override; + /** Used by ESPHome framework. */ + float get_setup_priority() const override; + + protected: + const char *name_; // TODO: extend esphome::Nameable + sensor::Sensor *full_spectrum_sensor_; + sensor::Sensor *infrared_sensor_; + sensor::Sensor *visible_sensor_; + sensor::Sensor *calculated_lux_sensor_; + TSL2591IntegrationTime integration_time_; + TSL2591Gain gain_; + bool power_save_mode_enabled_; + float device_factor_; + float glass_attenuation_factor_; + uint64_t interval_start_; + uint64_t interval_timeout_; + void disable_if_power_saving_(); + void process_update_(); + void interval_function_for_update_(); +}; + +} // namespace tsl2591 +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index 02fb92d305..a2f49fe3ca 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -96,6 +96,7 @@ CONF_BUFFER_SIZE = "buffer_size" CONF_BUILD_PATH = "build_path" CONF_BUS_VOLTAGE = "bus_voltage" CONF_BUSY_PIN = "busy_pin" +CONF_CALCULATED_LUX = "calculated_lux" CONF_CALIBRATE_LINEAR = "calibrate_linear" CONF_CALIBRATION = "calibration" CONF_CAPACITANCE = "capacitance" @@ -171,6 +172,7 @@ CONF_DELAY = "delay" CONF_DELTA = "delta" CONF_DEVICE = "device" CONF_DEVICE_CLASS = "device_class" +CONF_DEVICE_FACTOR = "device_factor" CONF_DIMENSIONS = "dimensions" CONF_DIO_PIN = "dio_pin" CONF_DIR_PIN = "dir_pin" @@ -244,11 +246,13 @@ CONF_FORMAT = "format" CONF_FORWARD_ACTIVE_ENERGY = "forward_active_energy" CONF_FREQUENCY = "frequency" CONF_FROM = "from" +CONF_FULL_SPECTRUM = "full_spectrum" CONF_FULL_UPDATE_EVERY = "full_update_every" CONF_GAIN = "gain" CONF_GAMMA_CORRECT = "gamma_correct" CONF_GAS_RESISTANCE = "gas_resistance" CONF_GATEWAY = "gateway" +CONF_GLASS_ATTENUATION_FACTOR = "glass_attenuation_factor" CONF_GLYPHS = "glyphs" CONF_GPIO = "gpio" CONF_GREEN = "green" @@ -287,6 +291,7 @@ CONF_IMPORT_REACTIVE_ENERGY = "import_reactive_energy" CONF_INCLUDES = "includes" CONF_INDEX = "index" CONF_INDOOR = "indoor" +CONF_INFRARED = "infrared" CONF_INITIAL_MODE = "initial_mode" CONF_INITIAL_OPTION = "initial_option" CONF_INITIAL_VALUE = "initial_value" @@ -660,6 +665,7 @@ CONF_VALUE = "value" CONF_VARIABLES = "variables" CONF_VARIANT = "variant" CONF_VERSION = "version" +CONF_VISIBLE = "visible" CONF_VISUAL = "visual" CONF_VOLTAGE = "voltage" CONF_VOLTAGE_ATTENUATION = "voltage_attenuation" @@ -696,6 +702,7 @@ ICON_BLUETOOTH = "mdi:bluetooth" ICON_BLUR = "mdi:blur" ICON_BRIEFCASE_DOWNLOAD = "mdi:briefcase-download" ICON_BRIGHTNESS_5 = "mdi:brightness-5" +ICON_BRIGHTNESS_6 = "mdi:brightness-6" ICON_BUG = "mdi:bug" ICON_CHECK_CIRCLE_OUTLINE = "mdi:check-circle-outline" ICON_CHEMICAL_WEAPON = "mdi:chemical-weapon" diff --git a/tests/test1.yaml b/tests/test1.yaml index 54f8eb589c..0e81b19998 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -840,6 +840,25 @@ sensor: is_cs_package: true integration_time: 402ms gain: 16x + - platform: tsl2591 + id: this_little_light_of_mine + address: 0x29 + update_interval: 15s + integration_time: 600ms + gain: high + visible: + name: "tsl2591 visible" + id: tsl2591_vis + unit_of_measurement: 'pH' + infrared: + name: "tsl2591 infrared" + id: tsl2591_ir + full_spectrum: + name: "tsl2591 full_spectrum" + id: tsl2591_fs + calculated_lux: + name: "tsl2591 calculated_lux" + id: tsl2591_cl - platform: ultrasonic trigger_pin: GPIO25 echo_pin: