diff --git a/CODEOWNERS b/CODEOWNERS index ebbc8732ea..83d64a8850 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -323,6 +323,7 @@ esphome/components/one_wire/* @ssieb esphome/components/online_image/* @clydebarrow @guillempages esphome/components/opentherm/* @olegtarasov esphome/components/openthread/* @mrene +esphome/components/opt3001/* @ccutrer esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core esphome/components/packet_transport/* @clydebarrow diff --git a/esphome/components/opt3001/__init__.py b/esphome/components/opt3001/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/opt3001/opt3001.cpp b/esphome/components/opt3001/opt3001.cpp new file mode 100644 index 0000000000..2d65f1090d --- /dev/null +++ b/esphome/components/opt3001/opt3001.cpp @@ -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 &f) { + // ensure the single shot flag is clear, indicating it's done + uint16_t raw_value; + if (this->read(reinterpret_cast(&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(&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 &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(&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 diff --git a/esphome/components/opt3001/opt3001.h b/esphome/components/opt3001/opt3001.h new file mode 100644 index 0000000000..ae3fde5c54 --- /dev/null +++ b/esphome/components/opt3001/opt3001.h @@ -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 &f); + // begins a one-shot measurement + void read_lx_(const std::function &f); + + bool updating_{false}; +}; + +} // namespace opt3001 +} // namespace esphome diff --git a/esphome/components/opt3001/sensor.py b/esphome/components/opt3001/sensor.py new file mode 100644 index 0000000000..a5bbf0e8dd --- /dev/null +++ b/esphome/components/opt3001/sensor.py @@ -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) diff --git a/tests/components/opt3001/common.yaml b/tests/components/opt3001/common.yaml new file mode 100644 index 0000000000..dab4f824f8 --- /dev/null +++ b/tests/components/opt3001/common.yaml @@ -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 diff --git a/tests/components/opt3001/test.esp32-ard.yaml b/tests/components/opt3001/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/opt3001/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/opt3001/test.esp32-c3-ard.yaml b/tests/components/opt3001/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/opt3001/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/opt3001/test.esp32-c3-idf.yaml b/tests/components/opt3001/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/opt3001/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/opt3001/test.esp32-idf.yaml b/tests/components/opt3001/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/opt3001/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/opt3001/test.esp8266-ard.yaml b/tests/components/opt3001/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/opt3001/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/opt3001/test.rp2040-ard.yaml b/tests/components/opt3001/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/opt3001/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml