tcs34725: Replace clear channel with sensor saturation with more accurate overexposure detection

- Removes the clear channel and introduces sensor saturation as precentage value.
- Ensures values stays within 0-100% range - fixes a rare bug where values would massively exceed 0-100% range, likely due to floating point matching of the exposure time failing.
- Export R, G, B values as 0.0, until next PR adds calibrated irradiance values.
This commit is contained in:
RubenKelevra 2024-09-27 08:51:34 +02:00
parent c019ff34bc
commit 021d593576
9 changed files with 100 additions and 70 deletions

View file

@ -10,6 +10,7 @@ from esphome.const import (
CONF_INTEGRATION_TIME,
DEVICE_CLASS_ILLUMINANCE,
ICON_LIGHTBULB,
ICON_GAUGE,
STATE_CLASS_MEASUREMENT,
UNIT_PERCENT,
ICON_THERMOMETER,
@ -23,6 +24,7 @@ CONF_RED_CHANNEL = "red_channel"
CONF_GREEN_CHANNEL = "green_channel"
CONF_BLUE_CHANNEL = "blue_channel"
CONF_CLEAR_CHANNEL = "clear_channel"
CONF_SENSOR_SATURATION = "sensor_saturation"
tcs34725_ns = cg.esphome_ns.namespace("tcs34725")
TCS34725Component = tcs34725_ns.class_(
@ -66,6 +68,12 @@ color_channel_schema = sensor.sensor_schema(
accuracy_decimals=1,
state_class=STATE_CLASS_MEASUREMENT,
)
sensor_saturation_schema = sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
icon=ICON_GAUGE,
accuracy_decimals=1,
state_class=STATE_CLASS_MEASUREMENT,
)
color_temperature_schema = sensor.sensor_schema(
unit_of_measurement=UNIT_KELVIN,
icon=ICON_THERMOMETER,
@ -86,7 +94,10 @@ CONFIG_SCHEMA = (
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_CLEAR_CHANNEL): color_channel_schema,
cv.Optional(CONF_CLEAR_CHANNEL): cv.invalid(
"The 'clear_channel' configuration option has been removed. Use 'sensor_saturation' instead."
),
cv.Optional(CONF_SENSOR_SATURATION): sensor_saturation_schema,
cv.Optional(CONF_ILLUMINANCE): illuminance_schema,
cv.Optional(CONF_COLOR_TEMPERATURE): color_temperature_schema,
cv.Optional(CONF_INTEGRATION_TIME, default="auto"): cv.enum(
@ -121,9 +132,9 @@ async def to_code(config):
if CONF_BLUE_CHANNEL in config:
sens = await sensor.new_sensor(config[CONF_BLUE_CHANNEL])
cg.add(var.set_blue_sensor(sens))
if CONF_CLEAR_CHANNEL in config:
sens = await sensor.new_sensor(config[CONF_CLEAR_CHANNEL])
cg.add(var.set_clear_sensor(sens))
if CONF_SENSOR_SATURATION in config:
sens = await sensor.new_sensor(config[CONF_SENSOR_SATURATION])
cg.add(var.set_sensor_saturation(sens))
if CONF_ILLUMINANCE in config:
sens = await sensor.new_sensor(config[CONF_ILLUMINANCE])
cg.add(var.set_illuminance_sensor(sens))

View file

@ -1,8 +1,9 @@
#include "tcs34725.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <algorithm>
#include "esphome/core/helpers.h"
#include <algorithm>
#include <cmath>
namespace esphome {
namespace tcs34725 {
@ -50,7 +51,7 @@ void TCS34725Component::dump_config() {
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Clear Channel", this->clear_sensor_);
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_);
@ -68,12 +69,16 @@ float TCS34725Component::get_setup_priority() const { return setup_priority::DAT
* Green value
* @param b
* Blue value
* @param c
* Clear channel value
* @param current_saturation
* Sensor saturation in percent
* @param min_raw_value
* lowest raw value reported by the sensor
* @return Color temperature in degrees Kelvin
*/
void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, uint16_t b, uint16_t c) {
float sat; /* Digital saturation level */
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;
@ -85,11 +90,18 @@ void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, u
static const float B_COEF = -0.444f; //
static const float CT_COEF = 3810.f; // Color Temperature Coefficient
static const float CT_OFFSET = 1391.f; // Color Temperatuer Offset
static const float MAX_ILLUMINANCE = 100000.0f; // Cap illuminance at 100,000 lux
static const float MAX_ILLUMINANCE = 200000.0f; // Cap illuminance at 200,000 lux
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
if (c == 0) {
// Minimum raw value below 1 is considered too low, return NaN
min_raw_limit = 1;
if (min_raw_value < min_raw_limit) {
ESP_LOGW(TAG,
"Saturation too low, sample with saturation %d (raw value) below limit (%d). Lux/color"
"temperature cannot reliably calculated.",
min_raw_value, min_raw_limit);
return;
}
@ -105,12 +117,12 @@ void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, u
* occur before analog saturation. Digital saturation occurs when
* the count reaches 65535.
*/
if ((256 - this->integration_reg_) > 63) {
/* Track digital saturation */
sat = 65535.f;
/* Adjust sat limit to 75% to avoid analog saturation if atime < 153.6ms */
if ((256 - this->integration_reg_) < 192) {
sat_limit = 99.99f;
} else {
/* Track analog saturation */
sat = 1024.f * (256.f - this->integration_reg_);
sat_limit = 75.0f;
}
/* Ripple rejection:
@ -130,20 +142,18 @@ void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, u
* ignored, but <= 150ms you should calculate the 75% saturation
* level to avoid this problem.
*/
if (this->integration_time_ < 150) {
/* Adjust sat to 75% to avoid analog saturation if atime < 153.6ms */
sat -= sat / 4.f;
}
/* Check for saturation and mark the sample as invalid if true */
if (c >= sat) {
if (current_saturation >= sat_limit) {
if (this->integration_time_auto_) {
ESP_LOGI(TAG, "Saturation too high, sample discarded, autogain ongoing");
return;
} else {
ESP_LOGW(TAG,
"Saturation too high, sample with saturation %.1f and clear %d lux/color temperature cannot reliably "
"calculated, reduce integration/gain or use a grey filter.",
sat, c);
"Saturation too high, sample with saturation %.1f above limit (%.1f). Lux/color"
"temperature cannot reliably calculated, reduce integration/gain or use a grey"
"filter.",
current_saturation, sat_limit);
return;
}
}
@ -200,24 +210,27 @@ void TCS34725Component::update() {
ESP_LOGV(TAG, "Raw values clear=%d red=%d green=%d blue=%d", raw_c, raw_r, raw_g, raw_b);
float channel_c;
float current_saturation;
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;
// avoid division by 0 and return black if clear is 0
if (raw_c == 0) {
channel_c = channel_r = channel_g = channel_b = 0.0f;
} else {
float max_count = this->integration_time_ <= 153.6f ? this->integration_time_ * 1024.0f / 2.4f : 65535.0f;
float sum = raw_c;
channel_r = raw_r / sum * 100.0f;
channel_g = raw_g / sum * 100.0f;
channel_b = raw_b / sum * 100.0f;
channel_c = raw_c / max_count * 100.0f;
}
if (this->clear_sensor_ != nullptr)
this->clear_sensor_->publish_state(channel_c);
/* 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. */
max_count = (this->integration_reg_ > 192)
? 65535
: (uint16_t) std::min(std::round(this->integration_time_ * 1024.0f / 2.4f), 65535.0f);
current_saturation = ((float) peak_raw_value / (float) max_count) * 100.0f;
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)
@ -226,49 +239,53 @@ void TCS34725Component::update() {
this->blue_sensor_->publish_state(channel_b);
if (this->illuminance_sensor_ || this->color_temperature_sensor_) {
calculate_temperature_and_lux_(raw_r, raw_g, raw_b, raw_c);
calculate_temperature_and_lux_(raw_r, raw_g, raw_b, current_saturation, min_raw_value);
}
// do not publish values if auto gain finding ongoing, and oversaturated
// so: publish when:
// - not auto mode
// - clear not oversaturated
// - clear oversaturated but gain and timing cannot go lower
if (!this->integration_time_auto_ || raw_c < 65530 || (this->gain_reg_ == 0 && this->integration_time_ < 200)) {
// - sensor not oversaturated
// - sensor oversaturated but gain and timing cannot go lower
if (!this->integration_time_auto_ || current_saturation < 99.99f ||
(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) {
this->sensor_saturation_->publish_state(current_saturation);
}
}
ESP_LOGD(TAG,
"Got Red=%.1f%%,Green=%.1f%%,Blue=%.1f%%,Clear=%.1f%% Illuminance=%.1flx Color "
"Got Red=%.1f%%,Green=%.1f%%,Blue=%.1f%%,Sensor Saturation=%.1f%% Illuminance=%.1flx Color "
"Temperature=%.1fK",
channel_r, channel_g, channel_b, channel_c, this->illuminance_, this->color_temperature_);
channel_r, channel_g, channel_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
// calculate optimal integration time to achieve 70% satuaration
// calculate optimal integration time to achieve 60% saturation
float integration_time_ideal;
integration_time_ideal = 60 / ((float) std::max((uint16_t) 1, raw_c) / 655.35f) * this->integration_time_;
integration_time_ideal = 60 / ((float) std::max((uint16_t) 1, peak_raw_value) / 655.35f) * this->integration_time_;
uint8_t gain_reg_val_new = this->gain_reg_;
// increase gain if less than 20% of white channel used and high integration time
// increase only if not already maximum
// do not use max gain, as ist will not get better
// increase gain if peak value is less 20% of maximum and we're already using the highest
// integration time
if (this->gain_reg_ < 3) {
if (((float) raw_c / 655.35 < 20.f) && (this->integration_time_ > 600.f)) {
if (((float) peak_raw_value / 655.35 < 20.f) && (this->integration_time_ > 600.f)) {
gain_reg_val_new = this->gain_reg_ + 1;
// update integration time to new situation
integration_time_ideal = integration_time_ideal / 4;
}
}
// decrease gain, if very high clear values and integration times alreadey low
// decrease gain, if very high sensor values and integration times already low
if (this->gain_reg_ > 0) {
if (70 < ((float) raw_c / 655.35) && (this->integration_time_ < 200)) {
if (70 < ((float) peak_raw_value / 655.35) && (this->integration_time_ < 200)) {
gain_reg_val_new = this->gain_reg_ - 1;
// update integration time to new situation
integration_time_ideal = integration_time_ideal * 4;
@ -286,8 +303,9 @@ void TCS34725Component::update() {
// calculate register value from timing
uint8_t regval_atime = (uint8_t) (256.f - integration_time_next / 2.4f);
ESP_LOGD(TAG, "Integration time: %.1fms, ideal: %.1fms regval_new %d Gain: %.f Clear channel raw: %d gain reg: %d",
this->integration_time_, integration_time_next, regval_atime, this->gain_, raw_c, this->gain_reg_);
ESP_LOGD(TAG, "Integration time: %.1fms, ideal: %.1fms regval_new %d Gain: %.f Peak raw: %d gain reg: %d",
this->integration_time_, integration_time_next, regval_atime, this->gain_, peak_raw_value,
this->gain_reg_);
if (this->integration_reg_ != regval_atime || gain_reg_val_new != this->gain_reg_) {
this->integration_reg_ = regval_atime;

View file

@ -42,7 +42,7 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice {
void set_gain(TCS34725Gain gain);
void set_glass_attenuation_factor(float ga);
void set_clear_sensor(sensor::Sensor *clear_sensor) { clear_sensor_ = clear_sensor; }
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; }
@ -67,7 +67,7 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice {
i2c::ErrorCode write_config_register_(uint8_t a_register, uint8_t data) {
return this->write_register(a_register, &data, 1);
}
sensor::Sensor *clear_sensor_{nullptr};
sensor::Sensor *sensor_saturation_{nullptr};
sensor::Sensor *red_sensor_{nullptr};
sensor::Sensor *green_sensor_{nullptr};
sensor::Sensor *blue_sensor_{nullptr};
@ -81,7 +81,8 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice {
bool integration_time_auto_{true};
private:
void calculate_temperature_and_lux_(uint16_t r, uint16_t g, uint16_t b, uint16_t c);
void calculate_temperature_and_lux_(uint16_t r, uint16_t g, uint16_t b, float current_saturation,
uint16_t min_raw_value);
uint16_t integration_reg_;
uint8_t gain_reg_{TCS34725_GAIN_1X};
};

View file

@ -11,8 +11,8 @@ sensor:
name: Green Channel
blue_channel:
name: Blue Channel
clear_channel:
name: Clear Channel
sensor_saturation:
name: Sensor Saturation
illuminance:
name: Illuminance
color_temperature:

View file

@ -11,8 +11,8 @@ sensor:
name: Green Channel
blue_channel:
name: Blue Channel
clear_channel:
name: Clear Channel
sensor_saturation:
name: Sensor Saturation
illuminance:
name: Illuminance
color_temperature:

View file

@ -11,8 +11,8 @@ sensor:
name: Green Channel
blue_channel:
name: Blue Channel
clear_channel:
name: Clear Channel
sensor_saturation:
name: Sensor Saturation
illuminance:
name: Illuminance
color_temperature:

View file

@ -11,8 +11,8 @@ sensor:
name: Green Channel
blue_channel:
name: Blue Channel
clear_channel:
name: Clear Channel
sensor_saturation:
name: Sensor Saturation
illuminance:
name: Illuminance
color_temperature:

View file

@ -11,8 +11,8 @@ sensor:
name: Green Channel
blue_channel:
name: Blue Channel
clear_channel:
name: Clear Channel
sensor_saturation:
name: Sensor Saturation
illuminance:
name: Illuminance
color_temperature:

View file

@ -11,8 +11,8 @@ sensor:
name: Green Channel
blue_channel:
name: Blue Channel
clear_channel:
name: Clear Channel
sensor_saturation:
name: Sensor Saturation
illuminance:
name: Illuminance
color_temperature: