From 03cf2452e186d3330c7b9097e7b697af676476a2 Mon Sep 17 00:00:00 2001 From: RubenKelevra Date: Fri, 27 Sep 2024 08:51:34 +0200 Subject: [PATCH] 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. --- esphome/components/tcs34725/sensor.py | 19 ++- esphome/components/tcs34725/tcs34725.cpp | 120 ++++++++++-------- esphome/components/tcs34725/tcs34725.h | 7 +- tests/components/tcs34725/test.esp32-ard.yaml | 4 +- .../tcs34725/test.esp32-c3-ard.yaml | 4 +- .../tcs34725/test.esp32-c3-idf.yaml | 4 +- tests/components/tcs34725/test.esp32-idf.yaml | 4 +- .../components/tcs34725/test.esp8266-ard.yaml | 4 +- .../components/tcs34725/test.rp2040-ard.yaml | 4 +- 9 files changed, 100 insertions(+), 70 deletions(-) diff --git a/esphome/components/tcs34725/sensor.py b/esphome/components/tcs34725/sensor.py index d47e9a34c8..83f3a3dfa3 100644 --- a/esphome/components/tcs34725/sensor.py +++ b/esphome/components/tcs34725/sensor.py @@ -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)) diff --git a/esphome/components/tcs34725/tcs34725.cpp b/esphome/components/tcs34725/tcs34725.cpp index 0830004b5a..881bd802f4 100644 --- a/esphome/components/tcs34725/tcs34725.cpp +++ b/esphome/components/tcs34725/tcs34725.cpp @@ -1,8 +1,9 @@ #include "tcs34725.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" -#include #include "esphome/core/helpers.h" +#include +#include 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; diff --git a/esphome/components/tcs34725/tcs34725.h b/esphome/components/tcs34725/tcs34725.h index 23985e8221..b97f8254f2 100644 --- a/esphome/components/tcs34725/tcs34725.h +++ b/esphome/components/tcs34725/tcs34725.h @@ -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}; }; diff --git a/tests/components/tcs34725/test.esp32-ard.yaml b/tests/components/tcs34725/test.esp32-ard.yaml index 86ef82962e..9ca8c9a7bb 100644 --- a/tests/components/tcs34725/test.esp32-ard.yaml +++ b/tests/components/tcs34725/test.esp32-ard.yaml @@ -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: diff --git a/tests/components/tcs34725/test.esp32-c3-ard.yaml b/tests/components/tcs34725/test.esp32-c3-ard.yaml index 9b459c9104..ce75c4245c 100644 --- a/tests/components/tcs34725/test.esp32-c3-ard.yaml +++ b/tests/components/tcs34725/test.esp32-c3-ard.yaml @@ -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: diff --git a/tests/components/tcs34725/test.esp32-c3-idf.yaml b/tests/components/tcs34725/test.esp32-c3-idf.yaml index 9b459c9104..ce75c4245c 100644 --- a/tests/components/tcs34725/test.esp32-c3-idf.yaml +++ b/tests/components/tcs34725/test.esp32-c3-idf.yaml @@ -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: diff --git a/tests/components/tcs34725/test.esp32-idf.yaml b/tests/components/tcs34725/test.esp32-idf.yaml index 86ef82962e..9ca8c9a7bb 100644 --- a/tests/components/tcs34725/test.esp32-idf.yaml +++ b/tests/components/tcs34725/test.esp32-idf.yaml @@ -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: diff --git a/tests/components/tcs34725/test.esp8266-ard.yaml b/tests/components/tcs34725/test.esp8266-ard.yaml index 9b459c9104..ce75c4245c 100644 --- a/tests/components/tcs34725/test.esp8266-ard.yaml +++ b/tests/components/tcs34725/test.esp8266-ard.yaml @@ -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: diff --git a/tests/components/tcs34725/test.rp2040-ard.yaml b/tests/components/tcs34725/test.rp2040-ard.yaml index 9b459c9104..ce75c4245c 100644 --- a/tests/components/tcs34725/test.rp2040-ard.yaml +++ b/tests/components/tcs34725/test.rp2040-ard.yaml @@ -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: