[opt3001] New component (#6625)

Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
This commit is contained in:
Cody Cutrer 2025-06-23 13:31:20 -06:00 committed by GitHub
parent 87a7157fc4
commit a35e476be5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 225 additions and 0 deletions

View File

@ -323,6 +323,7 @@ esphome/components/one_wire/* @ssieb
esphome/components/online_image/* @clydebarrow @guillempages esphome/components/online_image/* @clydebarrow @guillempages
esphome/components/opentherm/* @olegtarasov esphome/components/opentherm/* @olegtarasov
esphome/components/openthread/* @mrene esphome/components/openthread/* @mrene
esphome/components/opt3001/* @ccutrer
esphome/components/ota/* @esphome/core esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core esphome/components/output/* @esphome/core
esphome/components/packet_transport/* @clydebarrow esphome/components/packet_transport/* @clydebarrow

View File

View File

@ -0,0 +1,122 @@
#include "opt3001.h"
#include "esphome/core/log.h"
namespace esphome {
namespace opt3001 {
static const char *const TAG = "opt3001.sensor";
static const uint8_t OPT3001_REG_RESULT = 0x00;
static const uint8_t OPT3001_REG_CONFIGURATION = 0x01;
// See datasheet for full description of each bit.
static const uint16_t OPT3001_CONFIGURATION_RANGE_FULL = 0b1100000000000000;
static const uint16_t OPT3001_CONFIGURATION_CONVERSION_TIME_800 = 0b100000000000;
static const uint16_t OPT3001_CONFIGURATION_CONVERSION_MODE_MASK = 0b11000000000;
static const uint16_t OPT3001_CONFIGURATION_CONVERSION_MODE_SINGLE_SHOT = 0b01000000000;
static const uint16_t OPT3001_CONFIGURATION_CONVERSION_MODE_SHUTDOWN = 0b00000000000;
// tl;dr: Configure an automatic-ranged, 800ms single shot reading,
// with INT processing disabled
static const uint16_t OPT3001_CONFIGURATION_FULL_RANGE_ONE_SHOT = OPT3001_CONFIGURATION_RANGE_FULL |
OPT3001_CONFIGURATION_CONVERSION_TIME_800 |
OPT3001_CONFIGURATION_CONVERSION_MODE_SINGLE_SHOT;
static const uint16_t OPT3001_CONVERSION_TIME_800 = 825; // give it 25 extra ms; it seems to not be ready quite often
/*
opt3001 properties:
- e (exponent) = high 4 bits of result register
- m (mantissa) = low 12 bits of result register
- formula: (0.01 * 2^e) * m lx
*/
void OPT3001Sensor::read_result_(const std::function<void(float)> &f) {
// ensure the single shot flag is clear, indicating it's done
uint16_t raw_value;
if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Reading configuration register failed");
f(NAN);
return;
}
raw_value = i2c::i2ctohs(raw_value);
if ((raw_value & OPT3001_CONFIGURATION_CONVERSION_MODE_MASK) != OPT3001_CONFIGURATION_CONVERSION_MODE_SHUTDOWN) {
// not ready; wait 10ms and try again
ESP_LOGW(TAG, "Data not ready; waiting 10ms");
this->set_timeout("opt3001_wait", 10, [this, f]() { read_result_(f); });
return;
}
if (this->read_register(OPT3001_REG_RESULT, reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Reading result register failed");
f(NAN);
return;
}
raw_value = i2c::i2ctohs(raw_value);
uint8_t exponent = raw_value >> 12;
uint16_t mantissa = raw_value & 0b111111111111;
double lx = 0.01 * pow(2.0, double(exponent)) * double(mantissa);
f(float(lx));
}
void OPT3001Sensor::read_lx_(const std::function<void(float)> &f) {
// turn on (after one-shot sensor automatically powers down)
uint16_t start_measurement = i2c::htoi2cs(OPT3001_CONFIGURATION_FULL_RANGE_ONE_SHOT);
if (this->write_register(OPT3001_REG_CONFIGURATION, reinterpret_cast<uint8_t *>(&start_measurement), 2) !=
i2c::ERROR_OK) {
ESP_LOGW(TAG, "Triggering one shot measurement failed");
f(NAN);
return;
}
this->set_timeout("read", OPT3001_CONVERSION_TIME_800, [this, f]() {
if (this->write(&OPT3001_REG_CONFIGURATION, 1, true) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Starting configuration register read failed");
f(NAN);
return;
}
this->read_result_(f);
});
}
void OPT3001Sensor::dump_config() {
LOG_SENSOR("", "OPT3001", this);
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
LOG_UPDATE_INTERVAL(this);
}
void OPT3001Sensor::update() {
// Set a flag and skip just in case the sensor isn't responding,
// and we just keep waiting for it in read_result_.
// This way we don't end up with potentially boundless "threads"
// using up memory and eventually crashing the device
if (this->updating_) {
return;
}
this->updating_ = true;
this->read_lx_([this](float val) {
this->updating_ = false;
if (std::isnan(val)) {
this->status_set_warning();
this->publish_state(NAN);
return;
}
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val);
this->status_clear_warning();
this->publish_state(val);
});
}
float OPT3001Sensor::get_setup_priority() const { return setup_priority::DATA; }
} // namespace opt3001
} // namespace esphome

View File

@ -0,0 +1,27 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace opt3001 {
/// This class implements support for the i2c-based OPT3001 ambient light sensor.
class OPT3001Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
public:
void dump_config() override;
void update() override;
float get_setup_priority() const override;
protected:
// checks if one-shot is complete before reading the result and returning it
void read_result_(const std::function<void(float)> &f);
// begins a one-shot measurement
void read_lx_(const std::function<void(float)> &f);
bool updating_{false};
};
} // namespace opt3001
} // namespace esphome

View File

@ -0,0 +1,35 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
DEVICE_CLASS_ILLUMINANCE,
STATE_CLASS_MEASUREMENT,
UNIT_LUX,
)
DEPENDENCIES = ["i2c"]
CODEOWNERS = ["@ccutrer"]
opt3001_ns = cg.esphome_ns.namespace("opt3001")
OPT3001Sensor = opt3001_ns.class_(
"OPT3001Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
OPT3001Sensor,
unit_of_measurement=UNIT_LUX,
accuracy_decimals=1,
device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x44))
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)

View File

@ -0,0 +1,10 @@
i2c:
- id: i2c_opt3001
scl: ${scl_pin}
sda: ${sda_pin}
sensor:
- platform: opt3001
name: Living Room Brightness
address: 0x44
update_interval: 30s

View File

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO16
sda_pin: GPIO17
<<: !include common.yaml

View File

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View File

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View File

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO16
sda_pin: GPIO17
<<: !include common.yaml

View File

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View File

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml