mirror of
https://github.com/esphome/esphome.git
synced 2024-12-22 13:34:54 +01:00
VEML7700 and VEML6030 light sensors (#6067)
* VEML7700 and VEML6030 light sensors * VEML7700 and VEML6030 light sensors - CODEOWNERS * VEML7700 and VEML6030 light sensors - tidy up * VEML7700 and VEML6030 light sensors - tidy up * VEML7700 tidy up * VEML7700 tidy up 4 * VEML7700 tidying up more * VEML7700 after review. non-blocking approach * VEML7700 CONSTANT_CASE * VEML7700 merge fix * VEML7700 pragma pack changed to attribute * VEML7700 pragma pack -> attribute * Minor publish split * minor * LOGD->LOGV * new school tests added * Discard changes to tests/test1.yaml --------- Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
This commit is contained in:
parent
782d662c20
commit
5b28bd3d97
12 changed files with 877 additions and 0 deletions
|
@ -380,6 +380,7 @@ esphome/components/ultrasonic/* @OttoWinter
|
|||
esphome/components/uponor_smatrix/* @kroimon
|
||||
esphome/components/vbus/* @ssieb
|
||||
esphome/components/veml3235/* @kbx81
|
||||
esphome/components/veml7700/* @latonita
|
||||
esphome/components/version/* @esphome/core
|
||||
esphome/components/voice_assistant/* @jesserockz
|
||||
esphome/components/wake_on_lan/* @willwill2will54
|
||||
|
|
1
esphome/components/veml7700/__init__.py
Normal file
1
esphome/components/veml7700/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
CODEOWNERS = ["@latonita"]
|
190
esphome/components/veml7700/sensor.py
Normal file
190
esphome/components/veml7700/sensor.py
Normal file
|
@ -0,0 +1,190 @@
|
|||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.const import (
|
||||
CONF_ACTUAL_GAIN,
|
||||
CONF_AUTO_MODE,
|
||||
CONF_FULL_SPECTRUM,
|
||||
CONF_GAIN,
|
||||
CONF_GLASS_ATTENUATION_FACTOR,
|
||||
CONF_ID,
|
||||
CONF_INFRARED,
|
||||
CONF_INTEGRATION_TIME,
|
||||
CONF_NAME,
|
||||
UNIT_LUX,
|
||||
UNIT_MILLISECOND,
|
||||
ICON_BRIGHTNESS_5,
|
||||
ICON_BRIGHTNESS_6,
|
||||
ICON_TIMER,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@latonita"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
UNIT_COUNTS = "#"
|
||||
ICON_MULTIPLICATION = "mdi:multiplication"
|
||||
ICON_BRIGHTNESS_7 = "mdi:brightness-7"
|
||||
|
||||
CONF_ACTUAL_INTEGRATION_TIME = "actual_integration_time"
|
||||
CONF_AMBIENT_LIGHT = "ambient_light"
|
||||
CONF_AMBIENT_LIGHT_COUNTS = "ambient_light_counts"
|
||||
CONF_FULL_SPECTRUM_COUNTS = "full_spectrum_counts"
|
||||
CONF_LUX_COMPENSATION = "lux_compensation"
|
||||
|
||||
veml7700_ns = cg.esphome_ns.namespace("veml7700")
|
||||
|
||||
VEML7700Component = veml7700_ns.class_(
|
||||
"VEML7700Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
Gain = veml7700_ns.enum("Gain")
|
||||
GAINS = {
|
||||
"1/8X": Gain.X_1_8,
|
||||
"1/4X": Gain.X_1_4,
|
||||
"1X": Gain.X_1,
|
||||
"2X": Gain.X_2,
|
||||
}
|
||||
|
||||
IntegrationTime = veml7700_ns.enum("IntegrationTime")
|
||||
INTEGRATION_TIMES = {
|
||||
25: IntegrationTime.INTEGRATION_TIME_25MS,
|
||||
50: IntegrationTime.INTEGRATION_TIME_50MS,
|
||||
100: IntegrationTime.INTEGRATION_TIME_100MS,
|
||||
200: IntegrationTime.INTEGRATION_TIME_200MS,
|
||||
400: IntegrationTime.INTEGRATION_TIME_400MS,
|
||||
800: IntegrationTime.INTEGRATION_TIME_800MS,
|
||||
}
|
||||
|
||||
|
||||
def validate_integration_time(value):
|
||||
value = cv.positive_time_period_milliseconds(value).total_milliseconds
|
||||
return cv.enum(INTEGRATION_TIMES, int=True)(value)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(VEML7700Component),
|
||||
cv.Optional(CONF_AUTO_MODE, default=True): cv.boolean,
|
||||
cv.Optional(CONF_GAIN, default="1/8X"): cv.enum(GAINS, upper=True),
|
||||
cv.Optional(
|
||||
CONF_INTEGRATION_TIME, default="100ms"
|
||||
): validate_integration_time,
|
||||
cv.Optional(CONF_LUX_COMPENSATION, default=True): cv.boolean,
|
||||
cv.Optional(CONF_GLASS_ATTENUATION_FACTOR, default=1.0): cv.float_range(
|
||||
min=1.0
|
||||
),
|
||||
cv.Optional(CONF_AMBIENT_LIGHT): cv.maybe_simple_value(
|
||||
sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_LUX,
|
||||
icon=ICON_BRIGHTNESS_6,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
key=CONF_NAME,
|
||||
),
|
||||
cv.Optional(CONF_AMBIENT_LIGHT_COUNTS): cv.maybe_simple_value(
|
||||
sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_COUNTS,
|
||||
icon=ICON_BRIGHTNESS_6,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
key=CONF_NAME,
|
||||
),
|
||||
cv.Optional(CONF_FULL_SPECTRUM): cv.maybe_simple_value(
|
||||
sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_LUX,
|
||||
icon=ICON_BRIGHTNESS_7,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
key=CONF_NAME,
|
||||
),
|
||||
cv.Optional(CONF_FULL_SPECTRUM_COUNTS): cv.maybe_simple_value(
|
||||
sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_COUNTS,
|
||||
icon=ICON_BRIGHTNESS_7,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
key=CONF_NAME,
|
||||
),
|
||||
cv.Optional(CONF_INFRARED): cv.maybe_simple_value(
|
||||
sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_LUX,
|
||||
icon=ICON_BRIGHTNESS_5,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
key=CONF_NAME,
|
||||
),
|
||||
cv.Optional(CONF_ACTUAL_GAIN): cv.maybe_simple_value(
|
||||
sensor.sensor_schema(
|
||||
icon=ICON_MULTIPLICATION,
|
||||
accuracy_decimals=3,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
key=CONF_NAME,
|
||||
),
|
||||
cv.Optional(CONF_ACTUAL_INTEGRATION_TIME): cv.maybe_simple_value(
|
||||
sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MILLISECOND,
|
||||
icon=ICON_TIMER,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
key=CONF_NAME,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x10)),
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
if als_config := config.get(CONF_AMBIENT_LIGHT):
|
||||
sens = await sensor.new_sensor(als_config)
|
||||
cg.add(var.set_ambient_light_sensor(sens))
|
||||
|
||||
if als_cnt_config := config.get(CONF_AMBIENT_LIGHT_COUNTS):
|
||||
sens = await sensor.new_sensor(als_cnt_config)
|
||||
cg.add(var.set_ambient_light_counts_sensor(sens))
|
||||
|
||||
if full_spect_config := config.get(CONF_FULL_SPECTRUM):
|
||||
sens = await sensor.new_sensor(full_spect_config)
|
||||
cg.add(var.set_white_sensor(sens))
|
||||
|
||||
if full_spect_cnt_config := config.get(CONF_FULL_SPECTRUM_COUNTS):
|
||||
sens = await sensor.new_sensor(full_spect_cnt_config)
|
||||
cg.add(var.set_white_counts_sensor(sens))
|
||||
|
||||
if infrared_config := config.get(CONF_INFRARED):
|
||||
sens = await sensor.new_sensor(infrared_config)
|
||||
cg.add(var.set_infrared_sensor(sens))
|
||||
|
||||
if act_gain_config := config.get(CONF_ACTUAL_GAIN):
|
||||
sens = await sensor.new_sensor(act_gain_config)
|
||||
cg.add(var.set_actual_gain_sensor(sens))
|
||||
|
||||
if act_itime_config := config.get(CONF_ACTUAL_INTEGRATION_TIME):
|
||||
sens = await sensor.new_sensor(act_itime_config)
|
||||
cg.add(var.set_actual_integration_time_sensor(sens))
|
||||
|
||||
cg.add(var.set_enable_automatic_mode(config[CONF_AUTO_MODE]))
|
||||
cg.add(var.set_enable_lux_compensation(config[CONF_LUX_COMPENSATION]))
|
||||
cg.add(var.set_gain(config[CONF_GAIN]))
|
||||
cg.add(var.set_integration_time(config[CONF_INTEGRATION_TIME]))
|
||||
cg.add(var.set_glass_attenuation_factor(config[CONF_GLASS_ATTENUATION_FACTOR]))
|
437
esphome/components/veml7700/veml7700.cpp
Normal file
437
esphome/components/veml7700/veml7700.cpp
Normal file
|
@ -0,0 +1,437 @@
|
|||
#include "veml7700.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace veml7700 {
|
||||
|
||||
static const char *const TAG = "veml7700";
|
||||
static const size_t VEML_REG_SIZE = 2;
|
||||
|
||||
static float reduce_to_zero(float a, float b) { return (a > b) ? (a - b) : 0; }
|
||||
|
||||
template<typename T, size_t size> T get_next(const T (&array)[size], const T val) {
|
||||
size_t i = 0;
|
||||
size_t idx = -1;
|
||||
while (idx == -1 && i < size) {
|
||||
if (array[i] == val) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (idx == -1 || i + 1 >= size)
|
||||
return val;
|
||||
return array[i + 1];
|
||||
}
|
||||
|
||||
template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) {
|
||||
size_t i = size - 1;
|
||||
size_t idx = -1;
|
||||
while (idx == -1 && i > 0) {
|
||||
if (array[i] == val) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
i--;
|
||||
}
|
||||
if (idx == -1 || i == 0)
|
||||
return val;
|
||||
return array[i - 1];
|
||||
}
|
||||
|
||||
static uint16_t get_itime_ms(IntegrationTime time) {
|
||||
uint16_t ms = 0;
|
||||
switch (time) {
|
||||
case INTEGRATION_TIME_100MS:
|
||||
ms = 100;
|
||||
break;
|
||||
case INTEGRATION_TIME_200MS:
|
||||
ms = 200;
|
||||
break;
|
||||
case INTEGRATION_TIME_400MS:
|
||||
ms = 400;
|
||||
break;
|
||||
case INTEGRATION_TIME_800MS:
|
||||
ms = 800;
|
||||
break;
|
||||
case INTEGRATION_TIME_50MS:
|
||||
ms = 50;
|
||||
break;
|
||||
case INTEGRATION_TIME_25MS:
|
||||
ms = 25;
|
||||
break;
|
||||
default:
|
||||
ms = 100;
|
||||
}
|
||||
return ms;
|
||||
}
|
||||
|
||||
static float get_gain_coeff(Gain gain) {
|
||||
static const float GAIN_FLOAT[GAINS_COUNT] = {1.0f, 2.0f, 0.125f, 0.25f};
|
||||
return GAIN_FLOAT[gain & 0b11];
|
||||
}
|
||||
|
||||
static const char *get_gain_str(Gain gain) {
|
||||
static const char *gain_str[GAINS_COUNT] = {"1x", "2x", "1/8x", "1/4x"};
|
||||
return gain_str[gain & 0b11];
|
||||
}
|
||||
|
||||
void VEML7700Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up VEML7700/6030...");
|
||||
|
||||
auto err = this->configure_();
|
||||
if (err != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Sensor configuration failed");
|
||||
this->mark_failed();
|
||||
} else {
|
||||
this->state_ = State::INITIAL_SETUP_COMPLETED;
|
||||
}
|
||||
}
|
||||
|
||||
void VEML7700Component::dump_config() {
|
||||
LOG_I2C_DEVICE(this);
|
||||
ESP_LOGCONFIG(TAG, " Automatic gain/time: %s", YESNO(this->automatic_mode_enabled_));
|
||||
if (!this->automatic_mode_enabled_) {
|
||||
ESP_LOGCONFIG(TAG, " Gain: %s", get_gain_str(this->gain_));
|
||||
ESP_LOGCONFIG(TAG, " Integration time: %d ms", get_itime_ms(this->integration_time_));
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Lux compensation: %s", YESNO(this->lux_compensation_enabled_));
|
||||
ESP_LOGCONFIG(TAG, " Glass attenuation factor: %f", this->glass_attenuation_factor_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
LOG_SENSOR(" ", "ALS channel lux", this->ambient_light_sensor_);
|
||||
LOG_SENSOR(" ", "ALS channel counts", this->ambient_light_counts_sensor_);
|
||||
LOG_SENSOR(" ", "WHITE channel lux", this->white_sensor_);
|
||||
LOG_SENSOR(" ", "WHITE channel counts", this->white_counts_sensor_);
|
||||
LOG_SENSOR(" ", "FAKE_IR channel lux", this->fake_infrared_sensor_);
|
||||
LOG_SENSOR(" ", "Actual gain", this->actual_gain_sensor_);
|
||||
LOG_SENSOR(" ", "Actual integration time", this->actual_integration_time_sensor_);
|
||||
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Communication with I2C VEML7700/6030 failed!");
|
||||
}
|
||||
}
|
||||
|
||||
void VEML7700Component::update() {
|
||||
if (this->is_ready() && this->state_ == State::IDLE) {
|
||||
ESP_LOGV(TAG, "Update: Initiating new data collection");
|
||||
|
||||
this->state_ = this->automatic_mode_enabled_ ? State::COLLECTING_DATA_AUTO : State::COLLECTING_DATA;
|
||||
|
||||
this->readings_.als_counts = 0;
|
||||
this->readings_.white_counts = 0;
|
||||
this->readings_.actual_time = this->integration_time_;
|
||||
this->readings_.actual_gain = this->gain_;
|
||||
this->readings_.als_lux = 0;
|
||||
this->readings_.white_lux = 0;
|
||||
this->readings_.fake_infrared_lux = 0;
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Update: Component not ready yet");
|
||||
}
|
||||
}
|
||||
|
||||
void VEML7700Component::loop() {
|
||||
ErrorCode err = i2c::ERROR_OK;
|
||||
|
||||
if (this->state_ == State::INITIAL_SETUP_COMPLETED) {
|
||||
// Datasheet: 2.5 ms before the first measurement is needed, allowing for the correct start of the signal processor
|
||||
// and oscillator.
|
||||
// Reality: wait for couple integration times to have first samples captured
|
||||
this->set_timeout(2 * this->integration_time_, [this]() { this->state_ = State::IDLE; });
|
||||
}
|
||||
|
||||
if (this->is_ready()) {
|
||||
switch (this->state_) {
|
||||
case State::IDLE:
|
||||
// doing nothing, having best time
|
||||
break;
|
||||
|
||||
case State::COLLECTING_DATA:
|
||||
err = this->read_sensor_output_(this->readings_);
|
||||
this->state_ = (err == i2c::ERROR_OK) ? State::DATA_COLLECTED : State::IDLE;
|
||||
break;
|
||||
|
||||
case State::COLLECTING_DATA_AUTO: // Automatic mode - we start here to reconfigure device first
|
||||
case State::DATA_COLLECTED:
|
||||
if (!this->are_adjustments_required_(this->readings_)) {
|
||||
this->state_ = State::READY_TO_PUBLISH_PART_1;
|
||||
} else {
|
||||
// if sensitivity adjustment needed -
|
||||
// shutdown device to change config and wait one integration time period
|
||||
this->state_ = State::ADJUSTMENT_IN_PROGRESS;
|
||||
err = this->reconfigure_time_and_gain_(this->readings_.actual_time, this->readings_.actual_gain, true);
|
||||
if (err == i2c::ERROR_OK) {
|
||||
this->set_timeout(1 * get_itime_ms(this->readings_.actual_time),
|
||||
[this]() { this->state_ = State::READY_TO_APPLY_ADJUSTMENTS; });
|
||||
} else {
|
||||
this->state_ = State::IDLE;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case State::ADJUSTMENT_IN_PROGRESS:
|
||||
// nothing to be done, just waiting for the timeout
|
||||
break;
|
||||
|
||||
case State::READY_TO_APPLY_ADJUSTMENTS:
|
||||
// second stage of sensitivity adjustment - turn device back on
|
||||
// and wait 2-3 integration time periods to get good data samples
|
||||
this->state_ = State::ADJUSTMENT_IN_PROGRESS;
|
||||
err = this->reconfigure_time_and_gain_(this->readings_.actual_time, this->readings_.actual_gain, false);
|
||||
if (err == i2c::ERROR_OK) {
|
||||
this->set_timeout(3 * get_itime_ms(this->readings_.actual_time),
|
||||
[this]() { this->state_ = State::COLLECTING_DATA; });
|
||||
} else {
|
||||
this->state_ = State::IDLE;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::READY_TO_PUBLISH_PART_1:
|
||||
this->status_clear_warning();
|
||||
|
||||
this->apply_lux_calculation_(this->readings_);
|
||||
this->apply_lux_compensation_(this->readings_);
|
||||
this->apply_glass_attenuation_(this->readings_);
|
||||
|
||||
this->publish_data_part_1_(this->readings_);
|
||||
this->state_ = State::READY_TO_PUBLISH_PART_2;
|
||||
break;
|
||||
|
||||
case State::READY_TO_PUBLISH_PART_2:
|
||||
this->publish_data_part_2_(this->readings_);
|
||||
this->state_ = State::READY_TO_PUBLISH_PART_3;
|
||||
break;
|
||||
|
||||
case State::READY_TO_PUBLISH_PART_3:
|
||||
this->publish_data_part_3_(this->readings_);
|
||||
this->state_ = State::IDLE;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (err != i2c::ERROR_OK)
|
||||
this->status_set_warning();
|
||||
}
|
||||
}
|
||||
|
||||
ErrorCode VEML7700Component::configure_() {
|
||||
ESP_LOGV(TAG, "Configure");
|
||||
|
||||
ConfigurationRegister als_conf{0};
|
||||
als_conf.ALS_INT_EN = false;
|
||||
als_conf.ALS_PERS = Persistence::PERSISTENCE_1;
|
||||
als_conf.ALS_IT = this->integration_time_;
|
||||
als_conf.ALS_GAIN = this->gain_;
|
||||
|
||||
als_conf.ALS_SD = true;
|
||||
ESP_LOGV(TAG, "Shutdown before config. ALS_CONF_0 to 0x%04X", als_conf.raw);
|
||||
auto err = this->write_register((uint8_t) CommandRegisters::ALS_CONF_0, als_conf.raw_bytes, VEML_REG_SIZE);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Failed to shutdown, I2C error %d", err);
|
||||
return err;
|
||||
}
|
||||
delay(3);
|
||||
|
||||
als_conf.ALS_SD = false;
|
||||
ESP_LOGV(TAG, "Turning on. Setting ALS_CONF_0 to 0x%04X", als_conf.raw);
|
||||
err = this->write_register((uint8_t) CommandRegisters::ALS_CONF_0, als_conf.raw_bytes, VEML_REG_SIZE);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Failed to turn on, I2C error %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
PSMRegister psm{0};
|
||||
psm.PSM = PSM::PSM_MODE_1;
|
||||
psm.PSM_EN = false;
|
||||
ESP_LOGV(TAG, "Setting PSM to 0x%04X", psm.raw);
|
||||
err = this->write_register((uint8_t) CommandRegisters::PWR_SAVING, psm.raw_bytes, VEML_REG_SIZE);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Failed to set PSM, I2C error %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
ErrorCode VEML7700Component::reconfigure_time_and_gain_(IntegrationTime time, Gain gain, bool shutdown) {
|
||||
ESP_LOGV(TAG, "Reconfigure time and gain (%d ms, %s) %s", get_itime_ms(time), get_gain_str(gain),
|
||||
shutdown ? "Shutting down" : "Turning back on");
|
||||
|
||||
ConfigurationRegister als_conf{0};
|
||||
als_conf.raw = 0;
|
||||
|
||||
// We have to before changing parameters
|
||||
als_conf.ALS_SD = shutdown;
|
||||
als_conf.ALS_INT_EN = false;
|
||||
als_conf.ALS_PERS = Persistence::PERSISTENCE_1;
|
||||
als_conf.ALS_IT = time;
|
||||
als_conf.ALS_GAIN = gain;
|
||||
auto err = this->write_register((uint8_t) CommandRegisters::ALS_CONF_0, als_conf.raw_bytes, VEML_REG_SIZE);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "%s failed", shutdown ? "Shutdown" : "Turn on");
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
ErrorCode VEML7700Component::read_sensor_output_(Readings &data) {
|
||||
auto als_err =
|
||||
this->read_register((uint8_t) CommandRegisters::ALS, (uint8_t *) &data.als_counts, VEML_REG_SIZE, false);
|
||||
if (als_err != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Error reading ALS register, err = %d", als_err);
|
||||
}
|
||||
auto white_err =
|
||||
this->read_register((uint8_t) CommandRegisters::WHITE, (uint8_t *) &data.white_counts, VEML_REG_SIZE, false);
|
||||
if (white_err != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Error reading WHITE register, err = %d", white_err);
|
||||
}
|
||||
|
||||
ConfigurationRegister conf{0};
|
||||
auto err =
|
||||
this->read_register((uint8_t) CommandRegisters::ALS_CONF_0, (uint8_t *) conf.raw_bytes, VEML_REG_SIZE, false);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Error reading ALS_CONF_0 register, err = %d", white_err);
|
||||
}
|
||||
data.actual_time = conf.ALS_IT;
|
||||
data.actual_gain = conf.ALS_GAIN;
|
||||
|
||||
ESP_LOGV(TAG, "Data from sensors: ALS = %d, WHITE = %d, Gain = %s, Time = %d ms", data.als_counts, data.white_counts,
|
||||
get_gain_str(data.actual_gain), get_itime_ms(data.actual_time));
|
||||
return std::max(als_err, white_err);
|
||||
}
|
||||
|
||||
bool VEML7700Component::are_adjustments_required_(Readings &data) {
|
||||
// skip first sample in auto mode -
|
||||
// we need to reconfigure device after last measurement
|
||||
if (this->state_ == State::COLLECTING_DATA_AUTO)
|
||||
return true;
|
||||
|
||||
if (!this->automatic_mode_enabled_)
|
||||
return false;
|
||||
|
||||
// Recommended thresholds as per datasheet
|
||||
static constexpr uint16_t LOW_INTENSITY_THRESHOLD = 100;
|
||||
static constexpr uint16_t HIGH_INTENSITY_THRESHOLD = 10000;
|
||||
|
||||
static const IntegrationTime TIMES[INTEGRATION_TIMES_COUNT] = {INTEGRATION_TIME_25MS, INTEGRATION_TIME_50MS,
|
||||
INTEGRATION_TIME_100MS, INTEGRATION_TIME_200MS,
|
||||
INTEGRATION_TIME_400MS, INTEGRATION_TIME_800MS};
|
||||
static const Gain GAINS[GAINS_COUNT] = {X_1_8, X_1_4, X_1, X_2};
|
||||
|
||||
if (data.als_counts <= LOW_INTENSITY_THRESHOLD) {
|
||||
Gain next_gain = get_next(GAINS, data.actual_gain);
|
||||
if (next_gain != data.actual_gain) {
|
||||
data.actual_gain = next_gain;
|
||||
return true;
|
||||
}
|
||||
IntegrationTime next_time = get_next(TIMES, data.actual_time);
|
||||
if (next_time != data.actual_time) {
|
||||
data.actual_time = next_time;
|
||||
return true;
|
||||
}
|
||||
} else if (data.als_counts >= HIGH_INTENSITY_THRESHOLD) {
|
||||
Gain prev_gain = get_prev(GAINS, data.actual_gain);
|
||||
if (prev_gain != data.actual_gain) {
|
||||
data.actual_gain = prev_gain;
|
||||
return true;
|
||||
}
|
||||
IntegrationTime prev_time = get_prev(TIMES, data.actual_time);
|
||||
if (prev_time != data.actual_time) {
|
||||
data.actual_time = prev_time;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Counts are either good (between thresholds)
|
||||
// or there is no room to change sensitivity anymore
|
||||
return false;
|
||||
}
|
||||
|
||||
void VEML7700Component::apply_lux_calculation_(Readings &data) {
|
||||
static const float MAX_GAIN = 2.0f;
|
||||
static const float MAX_ITIME_MS = 800.0f;
|
||||
static const float MAX_LX_RESOLUTION = 0.0036f;
|
||||
float lux_resolution = (MAX_ITIME_MS / (float) get_itime_ms(data.actual_time)) *
|
||||
(MAX_GAIN / get_gain_coeff(data.actual_gain)) * MAX_LX_RESOLUTION;
|
||||
ESP_LOGV(TAG, "Lux resolution for (%d, %s) = %.4f ", get_itime_ms(data.actual_time), get_gain_str(data.actual_gain),
|
||||
lux_resolution);
|
||||
|
||||
data.als_lux = lux_resolution * (float) data.als_counts;
|
||||
data.white_lux = lux_resolution * (float) data.white_counts;
|
||||
data.fake_infrared_lux = reduce_to_zero(data.white_lux, data.als_lux);
|
||||
|
||||
ESP_LOGV(TAG, "%s mode - ALS = %.1f lx, WHITE = %.1f lx, FAKE_IR = %.1f lx",
|
||||
this->automatic_mode_enabled_ ? "Automatic" : "Manual", data.als_lux, data.white_lux,
|
||||
data.fake_infrared_lux);
|
||||
}
|
||||
|
||||
void VEML7700Component::apply_lux_compensation_(Readings &data) {
|
||||
if (!this->lux_compensation_enabled_)
|
||||
return;
|
||||
auto &local_data = data;
|
||||
// Always apply correction for G1/4 and G1/8
|
||||
// Other Gains G1 and G2 are not supposed to be used for lux > 1000,
|
||||
// corrections may help, but not a lot.
|
||||
//
|
||||
// "Illumination values higher than 1000 lx show non-linearity.
|
||||
// This non-linearity is the same for all sensors, so a compensation formula can be applied
|
||||
// if this light level is exceeded"
|
||||
auto compensate = [&local_data](float &lux) {
|
||||
auto calculate_high_lux_compensation = [](float lux_veml) -> float {
|
||||
return (((6.0135e-13 * lux_veml - 9.3924e-9) * lux_veml + 8.1488e-5) * lux_veml + 1.0023) * lux_veml;
|
||||
};
|
||||
|
||||
if (lux > 1000.0f || local_data.actual_gain == Gain::X_1_8 || local_data.actual_gain == Gain::X_1_4) {
|
||||
lux = calculate_high_lux_compensation(lux);
|
||||
}
|
||||
};
|
||||
|
||||
compensate(data.als_lux);
|
||||
compensate(data.white_lux);
|
||||
data.fake_infrared_lux = reduce_to_zero(data.white_lux, data.als_lux);
|
||||
|
||||
ESP_LOGV(TAG, "Lux compensation - ALS = %.1f lx, WHITE = %.1f lx, FAKE_IR = %.1f lx", data.als_lux, data.white_lux,
|
||||
data.fake_infrared_lux);
|
||||
}
|
||||
|
||||
void VEML7700Component::apply_glass_attenuation_(Readings &data) {
|
||||
data.als_lux *= this->glass_attenuation_factor_;
|
||||
data.white_lux *= this->glass_attenuation_factor_;
|
||||
data.fake_infrared_lux = reduce_to_zero(data.white_lux, data.als_lux);
|
||||
ESP_LOGV(TAG, "Glass attenuation - ALS = %.1f lx, WHITE = %.1f lx, FAKE_IR = %.1f lx", data.als_lux, data.white_lux,
|
||||
data.fake_infrared_lux);
|
||||
}
|
||||
|
||||
void VEML7700Component::publish_data_part_1_(Readings &data) {
|
||||
if (this->ambient_light_sensor_ != nullptr) {
|
||||
this->ambient_light_sensor_->publish_state(data.als_lux);
|
||||
}
|
||||
if (this->white_sensor_ != nullptr) {
|
||||
this->white_sensor_->publish_state(data.white_lux);
|
||||
}
|
||||
}
|
||||
|
||||
void VEML7700Component::publish_data_part_2_(Readings &data) {
|
||||
if (this->fake_infrared_sensor_ != nullptr) {
|
||||
this->fake_infrared_sensor_->publish_state(data.fake_infrared_lux);
|
||||
}
|
||||
if (this->ambient_light_counts_sensor_ != nullptr) {
|
||||
this->ambient_light_counts_sensor_->publish_state(data.als_counts);
|
||||
}
|
||||
if (this->white_counts_sensor_ != nullptr) {
|
||||
this->white_counts_sensor_->publish_state(data.white_counts);
|
||||
}
|
||||
}
|
||||
|
||||
void VEML7700Component::publish_data_part_3_(Readings &data) {
|
||||
if (this->actual_gain_sensor_ != nullptr) {
|
||||
this->actual_gain_sensor_->publish_state(get_gain_coeff(data.actual_gain));
|
||||
}
|
||||
if (this->actual_integration_time_sensor_ != nullptr) {
|
||||
this->actual_integration_time_sensor_->publish_state(get_itime_ms(data.actual_time));
|
||||
}
|
||||
}
|
||||
} // namespace veml7700
|
||||
} // namespace esphome
|
202
esphome/components/veml7700/veml7700.h
Normal file
202
esphome/components/veml7700/veml7700.h
Normal file
|
@ -0,0 +1,202 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/optional.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace veml7700 {
|
||||
|
||||
using esphome::i2c::ErrorCode;
|
||||
|
||||
//
|
||||
// Datasheet: https://www.vishay.com/docs/84286/veml7700.pdf
|
||||
//
|
||||
|
||||
enum class CommandRegisters : uint8_t {
|
||||
ALS_CONF_0 = 0x00, // W: ALS gain, integration time, interrupt, and shutdown
|
||||
ALS_WH = 0x01, // W: ALS high threshold window setting
|
||||
ALS_WL = 0x02, // W: ALS low threshold window setting
|
||||
PWR_SAVING = 0x03, // W: Set (15 : 3) 0000 0000 0000 0b
|
||||
ALS = 0x04, // R: MSB, LSB data of whole ALS 16 bits
|
||||
WHITE = 0x05, // R: MSB, LSB data of whole WHITE 16 bits
|
||||
ALS_INT = 0x06 // R: ALS INT trigger event
|
||||
};
|
||||
|
||||
enum Gain : uint8_t {
|
||||
X_1 = 0,
|
||||
X_2 = 1,
|
||||
X_1_8 = 2,
|
||||
X_1_4 = 3,
|
||||
};
|
||||
const uint8_t GAINS_COUNT = 4;
|
||||
|
||||
enum IntegrationTime : uint8_t {
|
||||
INTEGRATION_TIME_25MS = 0b1100,
|
||||
INTEGRATION_TIME_50MS = 0b1000,
|
||||
INTEGRATION_TIME_100MS = 0b0000,
|
||||
INTEGRATION_TIME_200MS = 0b0001,
|
||||
INTEGRATION_TIME_400MS = 0b0010,
|
||||
INTEGRATION_TIME_800MS = 0b0011,
|
||||
};
|
||||
const uint8_t INTEGRATION_TIMES_COUNT = 6;
|
||||
|
||||
enum Persistence : uint8_t {
|
||||
PERSISTENCE_1 = 0,
|
||||
PERSISTENCE_2 = 1,
|
||||
PERSISTENCE_4 = 2,
|
||||
PERSISTENCE_8 = 3,
|
||||
};
|
||||
|
||||
enum PSM : uint8_t {
|
||||
PSM_MODE_1 = 0,
|
||||
PSM_MODE_2 = 1,
|
||||
PSM_MODE_3 = 2,
|
||||
PSM_MODE_4 = 3,
|
||||
};
|
||||
|
||||
// The following section with bit-fields brings GCC compilation 'notes' about padding bytes due to bug in older GCC back
|
||||
// in 2009 "Packed bit-fields of type char were not properly bit-packed on many targets prior to GCC 4.4" Even more to
|
||||
// this - this message can't be disabled with "#pragma GCC diagnostic ignored" due to another bug which was only fixed
|
||||
// in GCC 13 in 2022 :) No actions required, it is just a note. The code is correct.
|
||||
|
||||
//
|
||||
// VEML7700_CR_ALS_CONF_0 Register (0x00)
|
||||
//
|
||||
union ConfigurationRegister {
|
||||
uint16_t raw;
|
||||
uint8_t raw_bytes[2];
|
||||
struct {
|
||||
bool ALS_SD : 1; // ALS shut down setting: 0 = ALS power on, 1 = ALS shut
|
||||
// down
|
||||
bool ALS_INT_EN : 1; // ALS interrupt enable setting: 0 = ALS INT disable, 1
|
||||
// = ALS INT enable
|
||||
bool reserved_2 : 1; // 0
|
||||
bool reserved_3 : 1; // 0
|
||||
Persistence ALS_PERS : 2; // 00 - 1, 01- 2, 10 - 4, 11 - 8
|
||||
IntegrationTime ALS_IT : 4; // ALS integration time setting
|
||||
bool reserved_10 : 1; // 0
|
||||
Gain ALS_GAIN : 2; // Gain selection
|
||||
bool reserved_13 : 1; // 0
|
||||
bool reserved_14 : 1; // 0
|
||||
bool reserved_15 : 1; // 0
|
||||
} __attribute__((packed));
|
||||
};
|
||||
|
||||
//
|
||||
// Power Saving Mode: PSM Register (0x03)
|
||||
//
|
||||
union PSMRegister {
|
||||
uint16_t raw;
|
||||
uint8_t raw_bytes[2];
|
||||
struct {
|
||||
bool PSM_EN : 1;
|
||||
uint8_t PSM : 2;
|
||||
uint16_t reserved : 13;
|
||||
} __attribute__((packed));
|
||||
};
|
||||
|
||||
class VEML7700Component : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
//
|
||||
// EspHome framework functions
|
||||
//
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
void loop() override;
|
||||
|
||||
//
|
||||
// Configuration setters
|
||||
//
|
||||
void set_gain(Gain gain) { this->gain_ = gain; }
|
||||
void set_integration_time(IntegrationTime time) { this->integration_time_ = time; }
|
||||
void set_enable_automatic_mode(bool enable) { this->automatic_mode_enabled_ = enable; }
|
||||
void set_enable_lux_compensation(bool enable) { this->lux_compensation_enabled_ = enable; }
|
||||
void set_glass_attenuation_factor(float factor) { this->glass_attenuation_factor_ = factor; }
|
||||
|
||||
void set_ambient_light_sensor(sensor::Sensor *sensor) { this->ambient_light_sensor_ = sensor; }
|
||||
void set_ambient_light_counts_sensor(sensor::Sensor *sensor) { this->ambient_light_counts_sensor_ = sensor; }
|
||||
void set_white_sensor(sensor::Sensor *sensor) { this->white_sensor_ = sensor; }
|
||||
void set_white_counts_sensor(sensor::Sensor *sensor) { this->white_counts_sensor_ = sensor; }
|
||||
void set_infrared_sensor(sensor::Sensor *sensor) { this->fake_infrared_sensor_ = sensor; }
|
||||
void set_actual_gain_sensor(sensor::Sensor *sensor) { this->actual_gain_sensor_ = sensor; }
|
||||
void set_actual_integration_time_sensor(sensor::Sensor *sensor) { this->actual_integration_time_sensor_ = sensor; }
|
||||
|
||||
protected:
|
||||
//
|
||||
// Internal state machine, used to split all the actions into
|
||||
// small steps in loop() to make sure we are not blocking execution
|
||||
//
|
||||
enum class State : uint8_t {
|
||||
NOT_INITIALIZED,
|
||||
INITIAL_SETUP_COMPLETED,
|
||||
IDLE,
|
||||
COLLECTING_DATA,
|
||||
COLLECTING_DATA_AUTO,
|
||||
DATA_COLLECTED,
|
||||
ADJUSTMENT_NEEDED,
|
||||
ADJUSTMENT_IN_PROGRESS,
|
||||
READY_TO_APPLY_ADJUSTMENTS,
|
||||
READY_TO_PUBLISH_PART_1,
|
||||
READY_TO_PUBLISH_PART_2,
|
||||
READY_TO_PUBLISH_PART_3
|
||||
} state_{State::NOT_INITIALIZED};
|
||||
|
||||
//
|
||||
// Current measurements data
|
||||
//
|
||||
struct Readings {
|
||||
uint16_t als_counts{0};
|
||||
uint16_t white_counts{0};
|
||||
IntegrationTime actual_time{INTEGRATION_TIME_100MS};
|
||||
Gain actual_gain{X_1_8};
|
||||
float als_lux{0};
|
||||
float white_lux{0};
|
||||
float fake_infrared_lux{0};
|
||||
ErrorCode err{i2c::ERROR_OK};
|
||||
} readings_;
|
||||
|
||||
//
|
||||
// Device interaction
|
||||
//
|
||||
ErrorCode configure_();
|
||||
ErrorCode reconfigure_time_and_gain_(IntegrationTime time, Gain gain, bool shutdown);
|
||||
ErrorCode read_sensor_output_(Readings &data);
|
||||
|
||||
//
|
||||
// Working with the data
|
||||
//
|
||||
bool are_adjustments_required_(Readings &data);
|
||||
void apply_lux_calculation_(Readings &data);
|
||||
void apply_lux_compensation_(Readings &data);
|
||||
void apply_glass_attenuation_(Readings &data);
|
||||
void publish_data_part_1_(Readings &data);
|
||||
void publish_data_part_2_(Readings &data);
|
||||
void publish_data_part_3_(Readings &data);
|
||||
|
||||
//
|
||||
// Component configuration
|
||||
//
|
||||
bool automatic_mode_enabled_{true};
|
||||
bool lux_compensation_enabled_{true};
|
||||
float glass_attenuation_factor_{1.0};
|
||||
IntegrationTime integration_time_{INTEGRATION_TIME_100MS};
|
||||
Gain gain_{X_1};
|
||||
|
||||
//
|
||||
// Sensors for publishing data
|
||||
//
|
||||
sensor::Sensor *ambient_light_sensor_{nullptr}; // Human eye range 500-600 nm, lx
|
||||
sensor::Sensor *ambient_light_counts_sensor_{nullptr}; // Raw counts
|
||||
sensor::Sensor *white_sensor_{nullptr}; // Wide range 450-950 nm, lx
|
||||
sensor::Sensor *white_counts_sensor_{nullptr}; // Raw counts
|
||||
sensor::Sensor *fake_infrared_sensor_{nullptr}; // Artificial. = WHITE lx - ALS lx.
|
||||
sensor::Sensor *actual_gain_sensor_{nullptr}; // Actual gain multiplier for the measurement
|
||||
sensor::Sensor *actual_integration_time_sensor_{nullptr}; // Actual integration time for the measurement
|
||||
};
|
||||
|
||||
} // namespace veml7700
|
||||
} // namespace esphome
|
10
tests/components/veml7700/common.yaml
Normal file
10
tests/components/veml7700/common.yaml
Normal file
|
@ -0,0 +1,10 @@
|
|||
sensor:
|
||||
- platform: veml7700
|
||||
address: 0x10
|
||||
i2c_id: i2c_veml7700
|
||||
ambient_light: Ambient light
|
||||
ambient_light_counts: Ambient light counts
|
||||
full_spectrum: Full spectrum
|
||||
full_spectrum_counts: Full spectrum counts
|
||||
actual_integration_time: Actual integration time
|
||||
actual_gain: Actual gain
|
6
tests/components/veml7700/test.esp32-c3-idf.yaml
Normal file
6
tests/components/veml7700/test.esp32-c3-idf.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
i2c:
|
||||
- id: i2c_veml7700
|
||||
scl: 5
|
||||
sda: 4
|
||||
|
||||
<<: !include common.yaml
|
6
tests/components/veml7700/test.esp32-c3.yaml
Normal file
6
tests/components/veml7700/test.esp32-c3.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
i2c:
|
||||
- id: i2c_veml7700
|
||||
scl: 5
|
||||
sda: 4
|
||||
|
||||
<<: !include common.yaml
|
6
tests/components/veml7700/test.esp32-idf.yaml
Normal file
6
tests/components/veml7700/test.esp32-idf.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
i2c:
|
||||
- id: i2c_veml7700
|
||||
scl: 16
|
||||
sda: 17
|
||||
|
||||
<<: !include common.yaml
|
6
tests/components/veml7700/test.esp32.yaml
Normal file
6
tests/components/veml7700/test.esp32.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
i2c:
|
||||
- id: i2c_veml7700
|
||||
scl: 16
|
||||
sda: 17
|
||||
|
||||
<<: !include common.yaml
|
6
tests/components/veml7700/test.esp8266.yaml
Normal file
6
tests/components/veml7700/test.esp8266.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
i2c:
|
||||
- id: i2c_veml7700
|
||||
scl: 5
|
||||
sda: 4
|
||||
|
||||
<<: !include common.yaml
|
6
tests/components/veml7700/test.rp2040.yaml
Normal file
6
tests/components/veml7700/test.rp2040.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
i2c:
|
||||
- id: i2c_veml7700
|
||||
scl: 5
|
||||
sda: 4
|
||||
|
||||
<<: !include common.yaml
|
Loading…
Reference in a new issue