diff --git a/CODEOWNERS b/CODEOWNERS index de1f3253d3..e22fa57ae4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -47,6 +47,7 @@ esphome/components/bang_bang/* @OttoWinter esphome/components/bedjet/* @jhansche esphome/components/bedjet/climate/* @jhansche esphome/components/bedjet/fan/* @jhansche +esphome/components/bh1745/* @latonita esphome/components/bh1750/* @OttoWinter esphome/components/binary_sensor/* @esphome/core esphome/components/bk72xx/* @kuba2k2 diff --git a/esphome/components/bh1745/__init__.py b/esphome/components/bh1745/__init__.py new file mode 100644 index 0000000000..dd06cfffea --- /dev/null +++ b/esphome/components/bh1745/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@latonita"] diff --git a/esphome/components/bh1745/bh1745.cpp b/esphome/components/bh1745/bh1745.cpp new file mode 100644 index 0000000000..e20d723b3e --- /dev/null +++ b/esphome/components/bh1745/bh1745.cpp @@ -0,0 +1,266 @@ +#include "bh1745.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include + +namespace esphome { +namespace bh1745 { + +static const char *const TAG = "bh1745"; + +static constexpr uint8_t BH1745_MANUFACTURER_ID = 0xE0; +static constexpr uint8_t BH1745_DEVICE_ID = 0b001011; +static constexpr uint8_t BH1745_RESET_TIMEOUT_MS = 100; +static constexpr uint8_t BH1745_BASE_MEAS_TIME_MS = 160; +static constexpr uint8_t BH1745_MAX_TRIES = 15; + +static constexpr float CHANNEL_COMPENSATION[BH1745_CHANNELS] = {2.2f, 1.0f, 1.8f, 10.0f}; + +uint32_t get_measurement_time_ms(MeasurementTime time) { + return ((uint32_t) BH1745_BASE_MEAS_TIME_MS) << static_cast(time); +} + +uint8_t get_adc_gain(AdcGain gain) { + switch (gain) { + case AdcGain::GAIN_1X: + return 1; + case AdcGain::GAIN_2X: + return 2; + case AdcGain::GAIN_16X: + return 16; + default: + return 1; + } +} + +void BH1745Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up BH1745"); + + uint8_t manuf_id = this->reg((uint8_t) Bh1745Registers::MANUFACTURER_ID).get(); + if (manuf_id != BH1745_MANUFACTURER_ID) { + ESP_LOGW(TAG, "Manufacturer ID of BH1745 is not correct! Got 0x%02X, expected 0x%02X", manuf_id, + BH1745_MANUFACTURER_ID); + this->mark_failed(); + return; + } + + SystemControlRegister sys_ctrl; + sys_ctrl.raw = this->reg((uint8_t) Bh1745Registers::SYSTEM_CONTROL).get(); + if (sys_ctrl.part_id != BH1745_DEVICE_ID) { + ESP_LOGW(TAG, "Device ID of BH1745 is not correct! Got 0x%02X, expected 0x%02X", sys_ctrl.part_id, + BH1745_DEVICE_ID); + this->mark_failed(); + return; + } + + sys_ctrl.sw_reset = true; + sys_ctrl.int_reset = false; + this->reg((uint8_t) Bh1745Registers::SYSTEM_CONTROL) = sys_ctrl.raw; + + this->set_timeout(BH1745_RESET_TIMEOUT_MS, [this]() { + this->configure_measurement_time_(); + this->state_ = State::DELAYED_SETUP; + }); +} + +void BH1745Component::dump_config() { + ESP_LOGCONFIG(TAG, "BH1745:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with BH1745 failed!"); + } + + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "Red Counts", this->red_counts_sensor_); + LOG_SENSOR(" ", "Green Counts", this->green_counts_sensor_); + LOG_SENSOR(" ", "Blue Counts", this->blue_counts_sensor_); + LOG_SENSOR(" ", "Clear Counts", this->clear_counts_sensor_); +} + +void BH1745Component::update() { + if (this->is_ready() && this->state_ == State::IDLE) { + ESP_LOGV(TAG, "Initiating new data collection"); + + this->state_ = State::MEASUREMENT_IN_PROGRESS; + + this->readings_.red = 0; + this->readings_.green = 0; + this->readings_.blue = 0; + this->readings_.clear = 0; + this->readings_.tries = 0; + + ModeControl2Register mode_ctrl2{0}; + mode_ctrl2.adc_gain = this->adc_gain_; + mode_ctrl2.rgbc_measurement_enable = true; + + this->reg((uint8_t) Bh1745Registers::MODE_CONTROL2) = mode_ctrl2.raw; + + this->set_timeout(get_measurement_time_ms(this->measurement_time_), + [this]() { this->state_ = State::WAITING_FOR_DATA; }); + } +} + +void BH1745Component::loop() { + switch (this->state_) { + case State::NOT_INITIALIZED: + break; + + case State::DELAYED_SETUP: + this->configure_gain_(); + this->reg((uint8_t) Bh1745Registers::MODE_CONTROL3) = 0x02; + this->set_timeout(BH1745_RESET_TIMEOUT_MS, [this]() { this->state_ = State::IDLE; }); + break; + + case State::IDLE: + break; + + case State::MEASUREMENT_IN_PROGRESS: + break; + + case State::WAITING_FOR_DATA: + if (this->is_data_ready_(this->readings_)) { + this->read_data_(this->readings_); + this->state_ = State::DATA_COLLECTED; + return; + } else if (this->readings_.tries > BH1745_MAX_TRIES) { + ESP_LOGW(TAG, "Can't get data after several tries. Aborting."); + this->status_set_warning(); + this->state_ = State::IDLE; + return; + } else { + this->readings_.tries++; + } + break; + + case State::DATA_COLLECTED: + this->publish_data_(); + break; + + default: + // wrong state + break; + } +} + +float BH1745Component::get_setup_priority() const { return setup_priority::DATA; } + +void BH1745Component::configure_measurement_time_() { + ModeControl1Register mode_ctrl1; + mode_ctrl1.reserved_3_7 = 0; + mode_ctrl1.measurement_time = this->measurement_time_; + this->reg((uint8_t) Bh1745Registers::MODE_CONTROL1) = mode_ctrl1.raw; +} + +void BH1745Component::configure_gain_() { + ModeControl2Register mode_ctrl2; + mode_ctrl2.raw = this->reg((uint8_t) Bh1745Registers::MODE_CONTROL2).get(); + mode_ctrl2.adc_gain = this->adc_gain_; + this->reg((uint8_t) Bh1745Registers::MODE_CONTROL2) = mode_ctrl2.raw; +} + +bool BH1745Component::is_data_ready_(Readings &data) { + ModeControl2Register mode_ctrl2; + mode_ctrl2.raw = this->reg((uint8_t) Bh1745Registers::MODE_CONTROL2).get(); + if (mode_ctrl2.valid) { + ModeControl1Register mode_ctrl1; + mode_ctrl1.raw = this->reg((uint8_t) Bh1745Registers::MODE_CONTROL1).get(); + + data.meas_time = mode_ctrl1.measurement_time; + data.gain = mode_ctrl2.adc_gain; + } + return mode_ctrl2.valid; +} + +void BH1745Component::read_data_(BH1745Component::Readings &data) { + static uint8_t buffer[BH1745_CHANNELS * 2]; + + this->read_bytes((uint8_t) Bh1745Registers::RED_DATA_LSB, buffer, BH1745_CHANNELS * 2); + data.red = ((buffer[1] << 8) + buffer[0]) & 0xffff; + data.green = ((buffer[3] << 8) + buffer[2]) & 0xffff; + data.blue = ((buffer[5] << 8) + buffer[4]) & 0xffff; + data.clear = ((buffer[7] << 8) + buffer[6]) & 0xffff; + + data.red *= CHANNEL_COMPENSATION[0]; + data.green *= CHANNEL_COMPENSATION[1]; + data.blue *= CHANNEL_COMPENSATION[2]; + data.clear *= CHANNEL_COMPENSATION[3]; + ESP_LOGD(TAG, "Red:%d,Green:%d,Blue:%d,Clear:%d", data.red, data.green, data.blue, data.clear); +} + +float BH1745Component::calculate_lux_(Readings &data) { + float lx, lx_tmp; + float gain = get_adc_gain(data.gain); + float integration_time = get_measurement_time_ms(data.meas_time); + + if (data.green < 1) { + lx_tmp = 0; + } else if (((float) data.clear / (float) data.green) < 0.160) { + lx_tmp = (0.202 * data.red + 0.766 * data.green); + } else { + lx_tmp = (0.159 * data.red + 0.646 * data.green); + } + if (lx_tmp < 0) { + lx_tmp = 0; + } + + lx = lx_tmp / gain / integration_time * 160; + ESP_LOGD(TAG, "Lux calculation:%.0f", lx); + return lx; +} + +float BH1745Component::calculate_cct_(Readings &data) { + uint32_t all = data.red + data.green + data.blue; + if (data.green < 1 || all < 1) + return 0; + float r_ratio = (float) data.red / all; + float b_ratio = (float) data.blue / all; + float ct = 0; + if (((float) data.clear / (float) data.green) < 0.160) { + float b_eff = fmin(b_ratio * 3.13, 1); + ct = ((1 - b_eff) * 12746 * (exp(-2.911 * r_ratio))) + (b_eff * 1637 * (exp(4.865 * b_ratio))); + + } else { + float b_eff = fmin(b_ratio * 10.67, 1); + ct = ((1 - b_eff) * 16234 * (exp(-2.781 * r_ratio))) + (b_eff * 1882 * (exp(4.448 * b_ratio))); + } + if (ct > 10000) + ct = 10000; + return roundf(ct); + /* + float cct; + float gain = get_adc_gain(data.gain); + float integration_time = get_measurement_time_ms(data.meas_time); + float r, g, b, x, y, n; + //copilot alg :) + r = (float)data.red / data.clear; + g = (float)data.green / data.clear; + b = (float)data.blue / data.clear; + + x = (-0.14282) * r + (1.54924) * g + (-0.95641) * b; + y = (-0.32466) * r + (1.57837) * g + (-0.73191) * b; + n = (x - 0.3320) / (0.1858 - y); + + cct = (449 * n * n * n + 3525 * n * n + 6823.3 * n + 5520.33) / gain / integration_time; + ESP_LOGD(TAG, "Red:%d,Green:%d,Blue:%d,Clear:%d,CCT calculation:%.0f\n", data.red, data.green, data.blue, + data.clear, cct); + */ +} + +void BH1745Component::publish_data_() { + if (this->red_counts_sensor_ != nullptr) { + this->red_counts_sensor_->publish_state(this->readings_.red); + } + if (this->green_counts_sensor_ != nullptr) { + this->green_counts_sensor_->publish_state(this->readings_.green); + } + if (this->blue_counts_sensor_ != nullptr) { + this->blue_counts_sensor_->publish_state(this->readings_.blue); + } + if (this->clear_counts_sensor_ != nullptr) { + this->clear_counts_sensor_->publish_state(this->readings_.clear); + } +} + +} // namespace bh1745 +} // namespace esphome diff --git a/esphome/components/bh1745/bh1745.h b/esphome/components/bh1745/bh1745.h new file mode 100644 index 0000000000..79fd31baa8 --- /dev/null +++ b/esphome/components/bh1745/bh1745.h @@ -0,0 +1,150 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace bh1745 { + +enum class Bh1745Registers : uint8_t { + SYSTEM_CONTROL = 0x40, + MODE_CONTROL1 = 0x41, + MODE_CONTROL2 = 0x42, + MODE_CONTROL3 = 0x44, + RED_DATA_LSB = 0x50, + RED_DATA_MSB = 0x51, + GREEN_DATA_LSB = 0x52, + GREEN_DATA_MSB = 0x53, + BLUE_DATA_LSB = 0x54, + BLUE_DATA_MSB = 0x55, + CLEAR_DATA_LSB = 0x56, + CLEAR_DATA_MSB = 0x57, + DINT_DATA_LSB = 0x58, + DINT_DATA_MSB = 0x59, + INTERRUPT_ = 0x60, + PERSISTENCE = 0x61, + TH_LSB = 0x62, + TH_MSB = 0x63, + TL_LSB = 0x64, + TL_MSB = 0x65, + MANUFACTURER_ID = 0x92, +}; + +enum MeasurementTime : uint8_t { + TIME_160MS = 0b000, + TIME_320MS = 0b001, + TIME_640MS = 0b010, + TIME_1280MS = 0b011, + TIME_2560MS = 0b100, + TIME_5120MS = 0b101, +}; + +enum AdcGain : uint8_t { + GAIN_1X = 0, + GAIN_2X, + GAIN_16X, +}; + +// 0x40 +union SystemControlRegister { + uint8_t raw; + struct { + uint8_t part_id : 6; + uint8_t int_reset : 1; + uint8_t sw_reset : 1; + }; +}; + +// 0x41 +union ModeControl1Register { + u_int8_t raw; + struct { + MeasurementTime measurement_time : 3; + uint8_t reserved_3_7 : 5; + }; +}; + +// 0x42 +union ModeControl2Register { + u_int8_t raw; + struct { + AdcGain adc_gain : 2; + uint8_t reserved_2_3 : 2; + bool rgbc_measurement_enable : 1; + uint8_t reserved_5_6 : 2; + bool valid : 1; + }; +}; + +constexpr uint8_t BH1745_CHANNELS = 4; + +// 0x44 +// always write 0x02 + +class BH1745Component : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + void update() override; + void loop() override; + float get_setup_priority() const override; + + void set_measurement_time(MeasurementTime measurement_time) { measurement_time_ = measurement_time; }; + void set_adc_gain(AdcGain adc_gain) { adc_gain_ = adc_gain; }; + + void set_red_counts_sensor(sensor::Sensor *red_counts_sensor) { red_counts_sensor_ = red_counts_sensor; } + void set_green_counts_sensor(sensor::Sensor *green_counts_sensor) { green_counts_sensor_ = green_counts_sensor; } + void set_blue_counts_sensor(sensor::Sensor *blue_counts_sensor) { blue_counts_sensor_ = blue_counts_sensor; } + void set_clear_counts_sensor(sensor::Sensor *clear_counts_sensor) { clear_counts_sensor_ = clear_counts_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; + } + + protected: + MeasurementTime measurement_time_{MeasurementTime::TIME_160MS}; + AdcGain adc_gain_{AdcGain::GAIN_1X}; + + sensor::Sensor *red_counts_sensor_{nullptr}; + sensor::Sensor *green_counts_sensor_{nullptr}; + sensor::Sensor *blue_counts_sensor_{nullptr}; + sensor::Sensor *clear_counts_sensor_{nullptr}; + sensor::Sensor *illuminance_sensor_{nullptr}; + sensor::Sensor *color_temperature_sensor_{nullptr}; + + enum class State : uint8_t { + NOT_INITIALIZED, + DELAYED_SETUP, + IDLE, + MEASUREMENT_IN_PROGRESS, + WAITING_FOR_DATA, + DATA_COLLECTED, + } state_{State::NOT_INITIALIZED}; + + struct Readings { + uint16_t red; + uint16_t green; + uint16_t blue; + uint16_t clear; + + AdcGain gain; + MeasurementTime meas_time; + + uint8_t tries; + } readings_; + + void configure_measurement_time_(); + void configure_gain_(); + + bool is_data_ready_(Readings &data); + void read_data_(Readings &data); + + float calculate_lux_(Readings &data); + float calculate_cct_(Readings &data); + + void publish_data_(); +}; + +} // namespace bh1745 +} // namespace esphome diff --git a/esphome/components/bh1745/sensor.py b/esphome/components/bh1745/sensor.py new file mode 100644 index 0000000000..ab75936c15 --- /dev/null +++ b/esphome/components/bh1745/sensor.py @@ -0,0 +1,131 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_COLOR_TEMPERATURE, + CONF_GAIN, + CONF_ILLUMINANCE, + CONF_INTEGRATION_TIME, + CONF_NAME, + ICON_LIGHTBULB, + ICON_THERMOMETER, + STATE_CLASS_MEASUREMENT, + DEVICE_CLASS_ILLUMINANCE, + UNIT_KELVIN, + UNIT_LUX, +) + +CODEOWNERS = ["@latonita"] +DEPENDENCIES = ["i2c"] + +UNIT_COUNTS = "#" + +CONF_RED_CHANNEL = "red_channel" +CONF_GREEN_CHANNEL = "green_channel" +CONF_BLUE_CHANNEL = "blue_channel" +CONF_CLEAR_CHANNEL = "clear_channel" + +bh1745_ns = cg.esphome_ns.namespace("bh1745") + +BH1745SComponent = bh1745_ns.class_( + "BH1745Component", cg.PollingComponent, i2c.I2CDevice +) + +AdcGain = bh1745_ns.enum("AdcGain") +ADC_GAINS = { + "1X": AdcGain.GAIN_1X, + "2X": AdcGain.GAIN_2X, + "16X": AdcGain.GAIN_16X, +} + +MeasurementTime = bh1745_ns.enum("MeasurementTime") +MEASUREMENT_TIMES = { + 160: MeasurementTime.TIME_160MS, + 320: MeasurementTime.TIME_320MS, + 640: MeasurementTime.TIME_640MS, + 1280: MeasurementTime.TIME_1280MS, + 2560: MeasurementTime.TIME_2560MS, + 5120: MeasurementTime.TIME_5120MS, +} + +color_channel_schema = cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_COUNTS, + icon=ICON_LIGHTBULB, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, +) + + +def validate_measurement_time(value): + value = cv.positive_time_period_milliseconds(value).total_milliseconds + return cv.enum(MEASUREMENT_TIMES, int=True)(value) + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BH1745SComponent), + cv.Optional(CONF_GAIN, default="1X"): cv.enum(ADC_GAINS, upper=True), + cv.Optional( + CONF_INTEGRATION_TIME, default="160ms" + ): validate_measurement_time, + 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_ILLUMINANCE): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_LUX, + icon=ICON_LIGHTBULB, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_COLOR_TEMPERATURE): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_KELVIN, + icon=ICON_THERMOMETER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x38)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + cg.add(var.set_adc_gain(config[CONF_GAIN])) + cg.add(var.set_measurement_time(config[CONF_INTEGRATION_TIME])) + + if CONF_RED_CHANNEL in config: + sens = await sensor.new_sensor(config[CONF_RED_CHANNEL]) + cg.add(var.set_red_counts_sensor(sens)) + if CONF_GREEN_CHANNEL in config: + sens = await sensor.new_sensor(config[CONF_GREEN_CHANNEL]) + cg.add(var.set_green_counts_sensor(sens)) + if CONF_BLUE_CHANNEL in config: + sens = await sensor.new_sensor(config[CONF_BLUE_CHANNEL]) + cg.add(var.set_blue_counts_sensor(sens)) + if CONF_CLEAR_CHANNEL in config: + sens = await sensor.new_sensor(config[CONF_CLEAR_CHANNEL]) + cg.add(var.set_clear_counts_sensor(sens)) + if CONF_ILLUMINANCE in config: + sens = await sensor.new_sensor(config[CONF_ILLUMINANCE]) + cg.add(var.set_illuminance_sensor(sens)) + if CONF_COLOR_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_COLOR_TEMPERATURE]) + cg.add(var.set_color_temperature_sensor(sens))