mirror of
https://github.com/esphome/esphome.git
synced 2024-11-21 11:37:27 +01:00
Support component tsl2591 (#2131)
Co-authored-by: Oxan van Leeuwen <oxan@oxanvanleeuwen.nl> Co-authored-by: WJCarpenter <bill@carpenter.org> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
parent
c6c2842bdb
commit
183e2a8471
@ -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
|
||||
|
@ -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):
|
||||
|
1
esphome/components/tsl2591/__init__.py
Normal file
1
esphome/components/tsl2591/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@wjcarpenter"]
|
168
esphome/components/tsl2591/sensor.py
Normal file
168
esphome/components/tsl2591/sensor.py
Normal file
@ -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]
|
||||
)
|
||||
)
|
369
esphome/components/tsl2591/tsl2591.cpp
Normal file
369
esphome/components/tsl2591/tsl2591.cpp
Normal file
@ -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
|
245
esphome/components/tsl2591/tsl2591.h
Normal file
245
esphome/components/tsl2591/tsl2591.h
Normal file
@ -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
|
@ -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"
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user