tcs34725: implement irradiance output per color channel

- Extracted responsivty curves for red, green, blue and clear channel from the specs
- Simulated three lights, specified in the spec sheet, emitting light onto clear responsivity curve, to get a median conversion factor for responsivity (in the graph) to counts/irradiance.
- Determine red, green, blue, clear channel range by FWHM
- Get the average responsivity for red, green, blue channels from the spec sheet curves and convert the units by the conversion factor determined ealier to counts/irradiance.
- Use channel range by FWHM to scale the responses accordingly
- Implement conversion from counts by the sensor to irradiance
This commit is contained in:
RubenKelevra 2024-09-29 16:16:54 +02:00
parent b61b97dc1f
commit b503e8a08d
10 changed files with 192 additions and 95 deletions

View file

@ -16,6 +16,7 @@ from esphome.const import (
ICON_THERMOMETER,
UNIT_KELVIN,
UNIT_LUX,
UNIT_IRRADIANCE,
)
DEPENDENCIES = ["i2c"]
@ -24,6 +25,9 @@ CONF_RED_CHANNEL = "red_channel"
CONF_GREEN_CHANNEL = "green_channel"
CONF_BLUE_CHANNEL = "blue_channel"
CONF_CLEAR_CHANNEL = "clear_channel"
CONF_RED_CHANNEL_IRRADIANCE = "red_channel_irradiance"
CONF_GREEN_CHANNEL_IRRADIANCE = "green_channel_irradiance"
CONF_BLUE_CHANNEL_IRRADIANCE = "blue_channel_irradiance"
CONF_SENSOR_SATURATION = "sensor_saturation"
tcs34725_ns = cg.esphome_ns.namespace("tcs34725")
@ -62,8 +66,8 @@ TCS34725_GAINS = {
"60X": TCS34725Gain.TCS34725_GAIN_60X,
}
color_channel_schema = sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
color_channel_irradiance_schema = sensor.sensor_schema(
unit_of_measurement=UNIT_IRRADIANCE,
icon=ICON_LIGHTBULB,
accuracy_decimals=1,
state_class=STATE_CLASS_MEASUREMENT,
@ -91,12 +95,21 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(TCS34725Component),
cv.Optional(CONF_RED_CHANNEL): color_channel_schema,
cv.Optional(CONF_GREEN_CHANNEL): color_channel_schema,
cv.Optional(CONF_BLUE_CHANNEL): color_channel_schema,
cv.Optional(CONF_RED_CHANNEL): cv.invalid(
"The 'red_channel' configuration option has been removed. Use 'red_channel_irradiance' instead."
),
cv.Optional(CONF_GREEN_CHANNEL): cv.invalid(
"The 'green_channel' configuration option has been removed. Use 'green_channel_irradiance' instead."
),
cv.Optional(CONF_BLUE_CHANNEL): cv.invalid(
"The 'blue_channel' configuration option has been removed. Use 'blue_channel_irradiance' instead."
),
cv.Optional(CONF_CLEAR_CHANNEL): cv.invalid(
"The 'clear_channel' configuration option has been removed. Use 'sensor_saturation' instead."
),
cv.Optional(CONF_RED_CHANNEL_IRRADIANCE): color_channel_irradiance_schema,
cv.Optional(CONF_GREEN_CHANNEL_IRRADIANCE): color_channel_irradiance_schema,
cv.Optional(CONF_BLUE_CHANNEL_IRRADIANCE): color_channel_irradiance_schema,
cv.Optional(CONF_SENSOR_SATURATION): sensor_saturation_schema,
cv.Optional(CONF_ILLUMINANCE): illuminance_schema,
cv.Optional(CONF_COLOR_TEMPERATURE): color_temperature_schema,
@ -123,15 +136,15 @@ async def to_code(config):
cg.add(var.set_gain(config[CONF_GAIN]))
cg.add(var.set_glass_attenuation_factor(config[CONF_GLASS_ATTENUATION_FACTOR]))
if CONF_RED_CHANNEL in config:
sens = await sensor.new_sensor(config[CONF_RED_CHANNEL])
cg.add(var.set_red_sensor(sens))
if CONF_GREEN_CHANNEL in config:
sens = await sensor.new_sensor(config[CONF_GREEN_CHANNEL])
cg.add(var.set_green_sensor(sens))
if CONF_BLUE_CHANNEL in config:
sens = await sensor.new_sensor(config[CONF_BLUE_CHANNEL])
cg.add(var.set_blue_sensor(sens))
if CONF_RED_CHANNEL_IRRADIANCE in config:
sens = await sensor.new_sensor(config[CONF_RED_CHANNEL_IRRADIANCE])
cg.add(var.set_red_irradiance_sensor(sens))
if CONF_GREEN_CHANNEL_IRRADIANCE in config:
sens = await sensor.new_sensor(config[CONF_GREEN_CHANNEL_IRRADIANCE])
cg.add(var.set_green_irradiance_sensor(sens))
if CONF_BLUE_CHANNEL_IRRADIANCE in config:
sens = await sensor.new_sensor(config[CONF_BLUE_CHANNEL_IRRADIANCE])
cg.add(var.set_blue_irradiance_sensor(sens))
if CONF_SENSOR_SATURATION in config:
sens = await sensor.new_sensor(config[CONF_SENSOR_SATURATION])
cg.add(var.set_sensor_saturation(sens))

View file

@ -17,6 +17,9 @@ static const uint8_t TCS34725_REGISTER_ATIME = TCS34725_COMMAND_BIT | 0x01;
static const uint8_t TCS34725_REGISTER_CONTROL = TCS34725_COMMAND_BIT | 0x0F;
static const uint8_t TCS34725_REGISTER_ENABLE = TCS34725_COMMAND_BIT | 0x00;
static const uint8_t TCS34725_REGISTER_CRGBDATAL = TCS34725_COMMAND_BIT | 0x14;
static const float RED_CHANNEL_COUNTS_TO_IRRADIANCE = 0.030895152730118627f; // counts/µW/cm²
static const float GREEN_CHANNEL_COUNTS_TO_IRRADIANCE = 0.032402966993759885f; // counts/µW/cm²
static const float BLUE_CHANNEL_COUNTS_TO_IRRADIANCE = 0.03695911040578352f; // counts/µW/cm²
void TCS34725Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up TCS34725...");
@ -52,14 +55,39 @@ void TCS34725Component::dump_config() {
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Sensor Saturation", this->sensor_saturation_);
LOG_SENSOR(" ", "Red Channel", this->red_sensor_);
LOG_SENSOR(" ", "Green Channel", this->green_sensor_);
LOG_SENSOR(" ", "Blue Channel", this->blue_sensor_);
LOG_SENSOR(" ", "Red Channel Irradiance", this->red_irradiance_sensor_);
LOG_SENSOR(" ", "Green Channel Irradiance", this->green_irradiance_sensor_);
LOG_SENSOR(" ", "Blue Channel Irradiance", this->blue_irradiance_sensor_);
LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_);
LOG_SENSOR(" ", "Color Temperature", this->color_temperature_sensor_);
}
float TCS34725Component::get_setup_priority() const { return setup_priority::DATA; }
/*!
* @brief Determines the minimum raw value threshold for color channels at which the sensor is considered underexposed
*
* @return Minimum raw value threshold
*/
uint16_t TCS34725Component::get_min_raw_limit_() const {
// Minimum raw value below 1 is considered too low, return NaN
return 1;
}
/*!
* @brief Determines the saturation threshold at which the sensor is considered overexposed
*
* @return The saturation limit as a percentage (between 0.0 and 100.0).
*/
float TCS34725Component::get_saturation_limit_() const {
// Return 99.99f if integration time is below 153.6ms, else return 75.0f
if ((256 - this->integration_reg_) < TCS34725_INTEGRATION_TIME_154MS) {
return 99.99f;
} else {
/* Adjust sat limit to 75% to avoid analog saturation if atime < 153.6ms */
return 75.0f;
}
}
/*!
* @brief Converts the raw R/G/B values to color temperature in degrees
* Kelvin using the algorithm described in DN40 from Taos (now AMS).
@ -77,9 +105,6 @@ float TCS34725Component::get_setup_priority() const { return setup_priority::DAT
*/
void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, uint16_t b, float current_saturation,
uint16_t min_raw_value) {
float sat_limit;
uint16_t min_raw_limit;
this->illuminance_ = NAN;
this->color_temperature_ = NAN;
@ -94,8 +119,7 @@ void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, u
static const float MAX_COLOR_TEMPERATURE = 15000.0f; // Maximum expected color temperature in Kelvin
static const float MIN_COLOR_TEMPERATURE = 1000.0f; // Maximum reasonable color temperature in Kelvin
// Minimum raw value below 1 is considered too low, return NaN
min_raw_limit = 1;
uint16_t min_raw_limit = get_min_raw_limit_();
if (min_raw_value < min_raw_limit) {
ESP_LOGW(TAG,
@ -118,12 +142,7 @@ void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, u
* the count reaches 65535.
*/
/* Adjust sat limit to 75% to avoid analog saturation if atime < 153.6ms */
if ((256 - this->integration_reg_) < 192) {
sat_limit = 99.99f;
} else {
sat_limit = 75.0f;
}
float sat_limit = get_saturation_limit_();
/* Ripple rejection:
*
@ -146,13 +165,12 @@ void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, u
/* Check for saturation and mark the sample as invalid if true */
if (current_saturation >= sat_limit) {
if (this->integration_time_auto_) {
ESP_LOGI(TAG, "Saturation too high, sample discarded, autogain ongoing");
ESP_LOGI(TAG, "Saturation too high, skip Lux/color temperature calculation, autogain ongoing");
return;
} else {
ESP_LOGW(TAG,
"Saturation too high, sample with saturation %.1f above limit (%.1f). Lux/color"
"temperature cannot reliably calculated, reduce integration/gain or use a grey"
"filter.",
"Saturation too high, sample with saturation %.1f above limit (%.1f). Lux/color temperature cannot be "
"reliably calculated, reduce integration/gain or use a grey filter.",
current_saturation, sat_limit);
return;
}
@ -192,6 +210,64 @@ void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, u
}
}
/*!
* @brief Calculates the irradiance per channel (R/G/B) using fixed conversion factors.
* @param r
* Red raw value
* @param g
* Green raw value
* @param b
* Blue raw value
* @param current_saturation
* Sensor saturation in percent
* @param min_raw_value
* Lowest raw value reported by the sensor
*/
void TCS34725Component::calculate_irradiance_(uint16_t r, uint16_t g, uint16_t b, float current_saturation,
uint16_t min_raw_value) {
this->irradiance_r_ = NAN;
this->irradiance_g_ = NAN;
this->irradiance_b_ = NAN;
uint16_t min_raw_limit = get_min_raw_limit_();
float sat_limit = get_saturation_limit_();
if (min_raw_value < min_raw_limit) {
ESP_LOGW(TAG,
"Saturation too low, sample with saturation %d (raw value) below limit (%d). Irradiance cannot be "
"reliably calculated.",
min_raw_value, min_raw_limit);
return;
}
/* Check for saturation and mark the sample as invalid if true */
if (current_saturation >= sat_limit) {
if (this->integration_time_auto_) {
ESP_LOGI(TAG, "Saturation too high, skip irradiance calculation, autogain ongoing");
return;
} else {
ESP_LOGW(TAG,
"Saturation too high, sample with saturation %.1f above limit (%.1f). Irradiance cannot be reliably "
"calculated, reduce integration/gain or use a grey filter.",
current_saturation, sat_limit);
return;
}
}
// Calculate the scaling factor for integration time
float integration_time_scaling = this->integration_time_ / 2.4f;
// Calculate irradiance for each channel using predefined conversion factors
this->irradiance_r_ = std::max(r / (RED_CHANNEL_COUNTS_TO_IRRADIANCE * integration_time_scaling * this->gain_), 0.0f);
this->irradiance_g_ =
std::max(g / (GREEN_CHANNEL_COUNTS_TO_IRRADIANCE * integration_time_scaling * this->gain_), 0.0f);
this->irradiance_b_ =
std::max(b / (BLUE_CHANNEL_COUNTS_TO_IRRADIANCE * integration_time_scaling * this->gain_), 0.0f);
ESP_LOGD(TAG, "Calculated irradiance - R: %.2f µW/cm2, G: %.2f µW/cm2, B: %.2f µW/cm2", this->irradiance_r_,
this->irradiance_g_, this->irradiance_b_);
}
void TCS34725Component::update() {
uint8_t data[8]; // Buffer to hold the 8 bytes (2 bytes for each of the 4 channels)
@ -214,9 +290,6 @@ void TCS34725Component::update() {
uint16_t peak_raw_value = std::max({raw_r, raw_g, raw_b});
uint16_t min_raw_value = std::min({raw_r, raw_g, raw_b});
uint16_t max_count;
float channel_r;
float channel_g;
float channel_b;
/* sensor counts up to 1024 for each 2.4 ms of integration time, until 65535 is hit, which is the
* maximum which can be stored in the counter. This happens at 153.6 ms integration time. */
@ -228,15 +301,9 @@ void TCS34725Component::update() {
current_saturation = clamp(current_saturation, 0.0f, 100.0f);
// FIXME: sum calculation cannot be done here anymore, so we publish 0 values for now
channel_r = channel_g = channel_b = 0.0f;
if (this->red_sensor_ != nullptr)
this->red_sensor_->publish_state(channel_r);
if (this->green_sensor_ != nullptr)
this->green_sensor_->publish_state(channel_g);
if (this->blue_sensor_ != nullptr)
this->blue_sensor_->publish_state(channel_b);
if (this->red_irradiance_sensor_ || this->green_irradiance_sensor_ || this->blue_irradiance_sensor_) {
calculate_irradiance_(raw_r, raw_g, raw_b, current_saturation, min_raw_value);
}
if (this->illuminance_sensor_ || this->color_temperature_sensor_) {
calculate_temperature_and_lux_(raw_r, raw_g, raw_b, current_saturation, min_raw_value);
@ -251,19 +318,23 @@ void TCS34725Component::update() {
(this->gain_reg_ == 0 && this->integration_time_ < 200)) {
if (this->illuminance_sensor_ != nullptr)
this->illuminance_sensor_->publish_state(this->illuminance_);
if (this->color_temperature_sensor_ != nullptr)
this->color_temperature_sensor_->publish_state(this->color_temperature_);
if (this->sensor_saturation_ != nullptr) {
if (this->sensor_saturation_ != nullptr)
this->sensor_saturation_->publish_state(current_saturation);
}
if (this->red_irradiance_sensor_ != nullptr)
this->red_irradiance_sensor_->publish_state(this->irradiance_r_);
if (this->green_irradiance_sensor_ != nullptr)
this->green_irradiance_sensor_->publish_state(this->irradiance_g_);
if (this->blue_irradiance_sensor_ != nullptr)
this->blue_irradiance_sensor_->publish_state(this->irradiance_b_);
}
ESP_LOGD(TAG,
"Got Red=%.1f%%,Green=%.1f%%,Blue=%.1f%%,Sensor Saturation=%.1f%% Illuminance=%.1flx Color "
"Temperature=%.1fK",
channel_r, channel_g, channel_b, current_saturation, this->illuminance_, this->color_temperature_);
"Calculated: Red Irad=%.2f µW/cm², Green Irad=%.2f µW/cm², Blue Irad=%.2f µW/cm², Sensor Sat=%.2f%%, "
"Illum=%.1f lx, Color Temp=%.1f K",
this->irradiance_r_, this->irradiance_g_, this->irradiance_b_, current_saturation, this->illuminance_,
this->color_temperature_);
if (this->integration_time_auto_) {
// change integration time an gain to achieve maximum resolution an dynamic range

View file

@ -43,9 +43,15 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice {
void set_glass_attenuation_factor(float ga);
void set_sensor_saturation(sensor::Sensor *sensor_saturation) { sensor_saturation_ = sensor_saturation; }
void set_red_sensor(sensor::Sensor *red_sensor) { red_sensor_ = red_sensor; }
void set_green_sensor(sensor::Sensor *green_sensor) { green_sensor_ = green_sensor; }
void set_blue_sensor(sensor::Sensor *blue_sensor) { blue_sensor_ = blue_sensor; }
void set_red_irradiance_sensor(sensor::Sensor *red_irradiance_sensor) {
red_irradiance_sensor_ = red_irradiance_sensor;
}
void set_green_irradiance_sensor(sensor::Sensor *green_irradiance_sensor) {
green_irradiance_sensor_ = green_irradiance_sensor;
}
void set_blue_irradiance_sensor(sensor::Sensor *blue_irradiance_sensor) {
blue_irradiance_sensor_ = blue_irradiance_sensor;
}
void set_illuminance_sensor(sensor::Sensor *illuminance_sensor) { illuminance_sensor_ = illuminance_sensor; }
void set_color_temperature_sensor(sensor::Sensor *color_temperature_sensor) {
color_temperature_sensor_ = color_temperature_sensor;
@ -68,21 +74,27 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice {
return this->write_register(a_register, &data, 1);
}
sensor::Sensor *sensor_saturation_{nullptr};
sensor::Sensor *red_sensor_{nullptr};
sensor::Sensor *green_sensor_{nullptr};
sensor::Sensor *blue_sensor_{nullptr};
sensor::Sensor *red_irradiance_sensor_{nullptr};
sensor::Sensor *green_irradiance_sensor_{nullptr};
sensor::Sensor *blue_irradiance_sensor_{nullptr};
sensor::Sensor *illuminance_sensor_{nullptr};
sensor::Sensor *color_temperature_sensor_{nullptr};
float integration_time_{2.4};
float gain_{1.0};
float glass_attenuation_{1.0};
float illuminance_;
float color_temperature_;
float illuminance_{NAN};
float color_temperature_{NAN};
float irradiance_r_{NAN};
float irradiance_g_{NAN};
float irradiance_b_{NAN};
bool integration_time_auto_{true};
private:
void calculate_temperature_and_lux_(uint16_t r, uint16_t g, uint16_t b, float current_saturation,
uint16_t min_raw_value);
void calculate_irradiance_(uint16_t r, uint16_t g, uint16_t b, float current_saturation, uint16_t min_raw_value);
float get_saturation_limit_() const;
uint16_t get_min_raw_limit_() const;
uint16_t integration_reg_;
uint8_t gain_reg_{TCS34725_GAIN_1X};
};

View file

@ -1048,6 +1048,7 @@ UNIT_GRAMS_PER_CUBIC_METER = "g/m³"
UNIT_HECTOPASCAL = "hPa"
UNIT_HERTZ = "Hz"
UNIT_HOUR = "h"
UNIT_IRRADIANCE = "µW/cm²"
UNIT_KELVIN = "K"
UNIT_KILOGRAM = "kg"
UNIT_KILOMETER = "km"

View file

@ -5,12 +5,12 @@ i2c:
sensor:
- platform: tcs34725
red_channel:
name: Red Channel
green_channel:
name: Green Channel
blue_channel:
name: Blue Channel
red_channel_irradiance:
name: Red Channel Irradiance
green_channel_irradiance:
name: Green Channel Irradiance
blue_channel_irradiance:
name: Blue Channel Irradiance
sensor_saturation:
name: Sensor Saturation
illuminance:

View file

@ -5,12 +5,12 @@ i2c:
sensor:
- platform: tcs34725
red_channel:
name: Red Channel
green_channel:
name: Green Channel
blue_channel:
name: Blue Channel
red_channel_irradiance:
name: Red Channel Irradiance
green_channel_irradiance:
name: Green Channel Irradiance
blue_channel_irradiance:
name: Blue Channel Irradiance
sensor_saturation:
name: Sensor Saturation
illuminance:

View file

@ -5,12 +5,12 @@ i2c:
sensor:
- platform: tcs34725
red_channel:
name: Red Channel
green_channel:
name: Green Channel
blue_channel:
name: Blue Channel
red_channel_irradiance:
name: Red Channel Irradiance
green_channel_irradiance:
name: Green Channel Irradiance
blue_channel_irradiance:
name: Blue Channel Irradiance
sensor_saturation:
name: Sensor Saturation
illuminance:

View file

@ -5,12 +5,12 @@ i2c:
sensor:
- platform: tcs34725
red_channel:
name: Red Channel
green_channel:
name: Green Channel
blue_channel:
name: Blue Channel
red_channel_irradiance:
name: Red Channel Irradiance
green_channel_irradiance:
name: Green Channel Irradiance
blue_channel_irradiance:
name: Blue Channel Irradiance
sensor_saturation:
name: Sensor Saturation
illuminance:

View file

@ -5,12 +5,12 @@ i2c:
sensor:
- platform: tcs34725
red_channel:
name: Red Channel
green_channel:
name: Green Channel
blue_channel:
name: Blue Channel
red_channel_irradiance:
name: Red Channel Irradiance
green_channel_irradiance:
name: Green Channel Irradiance
blue_channel_irradiance:
name: Blue Channel Irradiance
sensor_saturation:
name: Sensor Saturation
illuminance:

View file

@ -5,12 +5,12 @@ i2c:
sensor:
- platform: tcs34725
red_channel:
name: Red Channel
green_channel:
name: Green Channel
blue_channel:
name: Blue Channel
red_channel_irradiance:
name: Red Channel Irradiance
green_channel_irradiance:
name: Green Channel Irradiance
blue_channel_irradiance:
name: Blue Channel Irradiance
sensor_saturation:
name: Sensor Saturation
illuminance: