diff --git a/esphome/components/tcs34725/sensor.py b/esphome/components/tcs34725/sensor.py index a2a1f0cb45..8d211b56ed 100644 --- a/esphome/components/tcs34725/sensor.py +++ b/esphome/components/tcs34725/sensor.py @@ -17,6 +17,8 @@ from esphome.const import ( UNIT_KELVIN, UNIT_LUX, UNIT_IRRADIANCE, + UNIT_EMPTY, + ICON_COLOR, ) DEPENDENCIES = ["i2c"] @@ -29,6 +31,9 @@ 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" +CONF_CIE1931_X = "cie1931_x" +CONF_CIE1931_Y = "cie1931_y" +CONF_CIE1931_Z = "cie1931_z" tcs34725_ns = cg.esphome_ns.namespace("tcs34725") TCS34725Component = tcs34725_ns.class_( @@ -90,6 +95,12 @@ illuminance_schema = sensor.sensor_schema( device_class=DEVICE_CLASS_ILLUMINANCE, state_class=STATE_CLASS_MEASUREMENT, ) +cie1931_schema = sensor.sensor_schema( + unit_of_measurement=UNIT_EMPTY, + icon=ICON_COLOR, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, +) CONFIG_SCHEMA = ( cv.Schema( @@ -113,6 +124,9 @@ CONFIG_SCHEMA = ( 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_CIE1931_X): cie1931_schema, + cv.Optional(CONF_CIE1931_Y): cie1931_schema, + cv.Optional(CONF_CIE1931_Z): cie1931_schema, cv.Optional(CONF_INTEGRATION_TIME, default="auto"): cv.enum( TCS34725_INTEGRATION_TIMES, lower=True ), @@ -136,6 +150,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_CIE1931_X in config: + sens = await sensor.new_sensor(config[CONF_CIE1931_X]) + cg.add(var.set_cie1931_x_sensor(sens)) + if CONF_CIE1931_Y in config: + sens = await sensor.new_sensor(config[CONF_CIE1931_Y]) + cg.add(var.set_cie1931_y_sensor(sens)) + if CONF_CIE1931_Z in config: + sens = await sensor.new_sensor(config[CONF_CIE1931_Z]) + cg.add(var.set_cie1931_z_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)) diff --git a/esphome/components/tcs34725/tcs34725.cpp b/esphome/components/tcs34725/tcs34725.cpp index 460947675f..40ea6acbe7 100644 --- a/esphome/components/tcs34725/tcs34725.cpp +++ b/esphome/components/tcs34725/tcs34725.cpp @@ -58,6 +58,9 @@ void TCS34725Component::dump_config() { 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(" ", "CIE1931 X", this->cie1931_x_sensor_); + LOG_SENSOR(" ", "CIE1931 Y", this->cie1931_y_sensor_); + LOG_SENSOR(" ", "CIE1931 Z", this->cie1931_z_sensor_); LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_); LOG_SENSOR(" ", "Color Temperature", this->color_temperature_sensor_); } @@ -270,6 +273,64 @@ void TCS34725Component::calculate_irradiance_(uint16_t r, uint16_t g, uint16_t b this->irradiance_g_, this->irradiance_b_); } +void TCS34725Component::calculate_cie1931_(uint16_t r, uint16_t g, uint16_t b, float current_saturation, + uint16_t min_raw_value) { + this->cie1931_x_ = NAN; + this->cie1931_y_ = NAN; + this->cie1931_z_ = 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, + "Sensor Saturation too low, sample with saturation %d (raw value) below limit (%d). CIE1931 colors cannot " + "be reliably calculated.", + min_raw_value, min_raw_limit); + return; + } + + if (current_saturation >= sat_limit) { + if (this->integration_time_auto_) { + ESP_LOGI(TAG, "Saturation too high, skip CIE1931 calculation, autogain ongoing"); + return; + } else { + ESP_LOGW(TAG, + "Sensor Saturation too high, sample with saturation %.1f above limit (%.1f). CIE1931 colors cannot be " + "reliably calculated, reduce integration/gain or use a grey filter.", + current_saturation, sat_limit); + return; + } + } + + // CIE1931 transformation matrix + static const float C11 = 1.89883277f; + static const float C12 = -0.58883081f; + static const float C13 = 0.29150381f; + static const float C21 = -0.24840751f; + static const float C22 = 1.14289469f; + static const float C23 = 0.07957766f; + static const float C31 = -0.3095245f; + static const float C32 = 0.64687886f; + static const float C33 = 0.98121083f; + + // Calc scaling based on integration time + float integration_time_scaling = this->integration_time_ / 24.f; + float gain_scaling = this->gain_ / 16.f; + + // Adjust the raw RGB values based on the integration time scaling factor and gain + float scaled_r = (float) r / (integration_time_scaling * gain_scaling); + float scaled_g = (float) g / (integration_time_scaling * gain_scaling); + float scaled_b = (float) b / (integration_time_scaling * gain_scaling); + + // Calculate CIE1931 values + this->cie1931_x_ = std::max(C11 * scaled_r + C12 * scaled_g + C13 * scaled_b, 0.0f); + this->cie1931_y_ = std::max(C21 * scaled_r + C22 * scaled_g + C23 * scaled_b, 0.0f); + this->cie1931_z_ = std::max(C31 * scaled_r + C32 * scaled_g + C33 * scaled_b, 0.0f); + + ESP_LOGD(TAG, "Calculated CIE1931 - X: %.2f, Y: %.2f, Z: %.2f", this->cie1931_x_, this->cie1931_y_, this->cie1931_z_); +} + void TCS34725Component::update() { uint8_t data[8]; // Buffer to hold the 8 bytes (2 bytes for each of the 4 channels) @@ -307,6 +368,10 @@ void TCS34725Component::update() { calculate_irradiance_(raw_r, raw_g, raw_b, current_saturation, min_raw_value); } + if (this->cie1931_x_sensor_ || this->cie1931_y_sensor_ || this->cie1931_z_sensor_) { + calculate_cie1931_(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); } @@ -318,6 +383,12 @@ void TCS34725Component::update() { // - 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->cie1931_x_sensor_ != nullptr) + this->cie1931_x_sensor_->publish_state(this->cie1931_x_); + if (this->cie1931_y_sensor_ != nullptr) + this->cie1931_y_sensor_->publish_state(this->cie1931_y_); + if (this->cie1931_z_sensor_ != nullptr) + this->cie1931_z_sensor_->publish_state(this->cie1931_z_); if (this->illuminance_sensor_ != nullptr) this->illuminance_sensor_->publish_state(this->illuminance_); if (this->color_temperature_sensor_ != nullptr) @@ -334,9 +405,9 @@ void TCS34725Component::update() { ESP_LOGD(TAG, "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_); + "CIE1931 (X: %.2f, Y: %.2f, Z: %.2f), Illum=%.1f lx, Color Temp=%.1f K", + this->irradiance_r_, this->irradiance_g_, this->irradiance_b_, current_saturation, this->cie1931_x_, + this->cie1931_y_, this->cie1931_z_, this->illuminance_, this->color_temperature_); if (this->integration_time_auto_) { // change integration time an gain to achieve maximum resolution an dynamic range diff --git a/esphome/components/tcs34725/tcs34725.h b/esphome/components/tcs34725/tcs34725.h index 71b5eefadf..2806fe9a2e 100644 --- a/esphome/components/tcs34725/tcs34725.h +++ b/esphome/components/tcs34725/tcs34725.h @@ -56,6 +56,9 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice { void set_color_temperature_sensor(sensor::Sensor *color_temperature_sensor) { color_temperature_sensor_ = color_temperature_sensor; } + void set_cie1931_x_sensor(sensor::Sensor *cie1931_x_sensor) { cie1931_x_sensor_ = cie1931_x_sensor; } + void set_cie1931_y_sensor(sensor::Sensor *cie1931_y_sensor) { cie1931_y_sensor_ = cie1931_y_sensor; } + void set_cie1931_z_sensor(sensor::Sensor *cie1931_z_sensor) { cie1931_z_sensor_ = cie1931_z_sensor; } void setup() override; float get_setup_priority() const override; @@ -79,6 +82,9 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *blue_irradiance_sensor_{nullptr}; sensor::Sensor *illuminance_sensor_{nullptr}; sensor::Sensor *color_temperature_sensor_{nullptr}; + sensor::Sensor *cie1931_x_sensor_{nullptr}; + sensor::Sensor *cie1931_y_sensor_{nullptr}; + sensor::Sensor *cie1931_z_sensor_{nullptr}; float integration_time_{2.4}; float gain_{1.0}; float glass_attenuation_{1.0}; @@ -87,11 +93,15 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice { float irradiance_r_{NAN}; float irradiance_g_{NAN}; float irradiance_b_{NAN}; + float cie1931_x_{NAN}; + float cie1931_y_{NAN}; + float cie1931_z_{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_cie1931_(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; diff --git a/esphome/const.py b/esphome/const.py index 954567c77f..7ed99c8028 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -978,6 +978,7 @@ ICON_CELLPHONE_ARROW_DOWN = "mdi:cellphone-arrow-down" ICON_CHECK_CIRCLE_OUTLINE = "mdi:check-circle-outline" ICON_CHEMICAL_WEAPON = "mdi:chemical-weapon" ICON_CHIP = "mdi:chip" +ICON_COLOR = "mdi:color" ICON_COUNTER = "mdi:counter" ICON_CURRENT_AC = "mdi:current-ac" ICON_DATABASE = "mdi:database" diff --git a/tests/components/tcs34725/test.esp32-ard.yaml b/tests/components/tcs34725/test.esp32-ard.yaml index 12241c1b24..f77ef3849b 100644 --- a/tests/components/tcs34725/test.esp32-ard.yaml +++ b/tests/components/tcs34725/test.esp32-ard.yaml @@ -5,6 +5,12 @@ i2c: sensor: - platform: tcs34725 + cie1931_x: + name: CIE1931 X + cie1931_y: + name: CIE1931 Y + cie1931_z: + name: CIE1931 Z red_channel_irradiance: name: Red Channel Irradiance green_channel_irradiance: diff --git a/tests/components/tcs34725/test.esp32-c3-ard.yaml b/tests/components/tcs34725/test.esp32-c3-ard.yaml index 6177edd0a1..b68e83f571 100644 --- a/tests/components/tcs34725/test.esp32-c3-ard.yaml +++ b/tests/components/tcs34725/test.esp32-c3-ard.yaml @@ -5,6 +5,12 @@ i2c: sensor: - platform: tcs34725 + cie1931_x: + name: CIE1931 X + cie1931_y: + name: CIE1931 Y + cie1931_z: + name: CIE1931 Z red_channel_irradiance: name: Red Channel Irradiance green_channel_irradiance: diff --git a/tests/components/tcs34725/test.esp32-c3-idf.yaml b/tests/components/tcs34725/test.esp32-c3-idf.yaml index 6177edd0a1..b68e83f571 100644 --- a/tests/components/tcs34725/test.esp32-c3-idf.yaml +++ b/tests/components/tcs34725/test.esp32-c3-idf.yaml @@ -5,6 +5,12 @@ i2c: sensor: - platform: tcs34725 + cie1931_x: + name: CIE1931 X + cie1931_y: + name: CIE1931 Y + cie1931_z: + name: CIE1931 Z red_channel_irradiance: name: Red Channel Irradiance green_channel_irradiance: diff --git a/tests/components/tcs34725/test.esp32-idf.yaml b/tests/components/tcs34725/test.esp32-idf.yaml index 12241c1b24..f77ef3849b 100644 --- a/tests/components/tcs34725/test.esp32-idf.yaml +++ b/tests/components/tcs34725/test.esp32-idf.yaml @@ -5,6 +5,12 @@ i2c: sensor: - platform: tcs34725 + cie1931_x: + name: CIE1931 X + cie1931_y: + name: CIE1931 Y + cie1931_z: + name: CIE1931 Z red_channel_irradiance: name: Red Channel Irradiance green_channel_irradiance: diff --git a/tests/components/tcs34725/test.esp8266-ard.yaml b/tests/components/tcs34725/test.esp8266-ard.yaml index 6177edd0a1..b68e83f571 100644 --- a/tests/components/tcs34725/test.esp8266-ard.yaml +++ b/tests/components/tcs34725/test.esp8266-ard.yaml @@ -5,6 +5,12 @@ i2c: sensor: - platform: tcs34725 + cie1931_x: + name: CIE1931 X + cie1931_y: + name: CIE1931 Y + cie1931_z: + name: CIE1931 Z red_channel_irradiance: name: Red Channel Irradiance green_channel_irradiance: diff --git a/tests/components/tcs34725/test.rp2040-ard.yaml b/tests/components/tcs34725/test.rp2040-ard.yaml index 6177edd0a1..b68e83f571 100644 --- a/tests/components/tcs34725/test.rp2040-ard.yaml +++ b/tests/components/tcs34725/test.rp2040-ard.yaml @@ -5,6 +5,12 @@ i2c: sensor: - platform: tcs34725 + cie1931_x: + name: CIE1931 X + cie1931_y: + name: CIE1931 Y + cie1931_z: + name: CIE1931 Z red_channel_irradiance: name: Red Channel Irradiance green_channel_irradiance: