mirror of
https://github.com/esphome/esphome.git
synced 2024-11-27 09:18:00 +01:00
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:
parent
cedb671f07
commit
03cf2452e1
9 changed files with 100 additions and 70 deletions
|
@ -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))
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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};
|
||||
};
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue