diff --git a/CODEOWNERS b/CODEOWNERS index 2c637b0d20..99a12f839f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -40,6 +40,7 @@ esphome/components/analog_threshold/* @ianchi esphome/components/animation/* @syndlex esphome/components/anova/* @buxtronix esphome/components/apds9306/* @aodrenah +esphome/components/apds9930/* @iyesin esphome/components/api/* @esphome/core esphome/components/aqi/* @freekode @jasstrong @ximex esphome/components/as5600/* @ammmze diff --git a/esphome/components/apds9930/__init__.py b/esphome/components/apds9930/__init__.py new file mode 100644 index 0000000000..0e6330bd88 --- /dev/null +++ b/esphome/components/apds9930/__init__.py @@ -0,0 +1,2 @@ +CODEOWNERS = ["@iyesin"] +DOMAIN = "apds9930" diff --git a/esphome/components/apds9930/apds9930.cpp b/esphome/components/apds9930/apds9930.cpp new file mode 100644 index 0000000000..6debc0d9c7 --- /dev/null +++ b/esphome/components/apds9930/apds9930.cpp @@ -0,0 +1,140 @@ +#include "apds9930.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +#include +#include + +namespace esphome::apds9930 { + +ESPHOME_LOG_TAG(TAG, "apds9930"); + +// Register addresses +static constexpr uint8_t APDS9930_ENABLE = 0x00; +static constexpr uint8_t APDS9930_ATIME = 0x01; +static constexpr uint8_t APDS9930_PTIME = 0x02; +static constexpr uint8_t APDS9930_PPULSE = 0x0E; +static constexpr uint8_t APDS9930_CONTROL = 0x0F; +static constexpr uint8_t APDS9930_ID = 0x12; +static constexpr uint8_t APDS9930_STATUS = 0x13; + +// Command byte: repeated byte and auto-increment protocols +static constexpr uint8_t APDS9930_CMD = 0x80; +static constexpr uint8_t APDS9930_CMD_AUTO_INCREMENT = 0xA0; + +static constexpr uint8_t APDS9930_CHIP_ID = 0x39; + +// ENABLE register bits +static constexpr uint8_t APDS9930_PON = 0x01; +static constexpr uint8_t APDS9930_AEN = 0x02; +static constexpr uint8_t APDS9930_PEN = 0x04; + +// STATUS register bits +static constexpr uint8_t APDS9930_AVALID = 0x01; +static constexpr uint8_t APDS9930_PVALID = 0x02; + +// CONTROL register fields +static constexpr uint8_t CONTROL_PDRIVE_SHIFT = 6; +static constexpr uint8_t CONTROL_PDIODE_SHIFT = 4; +static constexpr uint8_t CONTROL_PGAIN_SHIFT = 2; +static constexpr uint8_t CONTROL_AGAIN_SHIFT = 0; +static constexpr uint8_t CONTROL_PDIODE_CH1 = 0b10; // The only supported proximity diode + +static constexpr uint8_t APDS9930_ATIME_VALUE = 0xED; // 19 cycles, about 52 ms +static constexpr uint8_t APDS9930_PTIME_VALUE = 0xFF; // 2.73 ms +static constexpr uint8_t APDS9930_PPULSE_VALUE = 0x08; // 8 pulses + +// Lux calculation coefficients +static constexpr float APDS9930_DF = 52.0f; +static constexpr float APDS9930_GA = 0.49f; +static constexpr float APDS9930_ALS_B = 1.862f; +static constexpr float APDS9930_ALS_C = 0.746f; +static constexpr float APDS9930_ALS_D = 1.291f; +static constexpr float APDS9930_ALSIT_MS = 2.73f * (256 - APDS9930_ATIME_VALUE); + +static constexpr uint8_t AMBIENT_GAINS[4] PROGMEM = {1, 8, 16, 120}; +static constexpr uint8_t PROXIMITY_GAINS[4] PROGMEM = {1, 2, 4, 8}; +static constexpr float LED_CURRENT_MAX_MA = 100.0f; // halved for each drive step + +// Lux per count at 1x gain and the fixed integration time +static constexpr float LUX_PER_COUNT_1X = APDS9930_GA * APDS9930_DF / APDS9930_ALSIT_MS; +// Each integration cycle adds up to 1024 counts, so this is the ceiling of both light channels +static constexpr uint16_t ALS_SATURATION = 1024 * (256 - APDS9930_ATIME_VALUE); + +bool APDS9930Component::write_reg_(uint8_t reg, uint8_t value) { return this->write_byte(reg | APDS9930_CMD, value); } + +void APDS9930Component::setup() { + uint8_t id; + if (!this->read_byte(APDS9930_ID | APDS9930_CMD, &id)) { + this->mark_failed(LOG_STR("Communication failed")); + return; + } + if (id != APDS9930_CHIP_ID) { + ESP_LOGE(TAG, "Wrong ID 0x%02X (expected 0x%02X)", id, APDS9930_CHIP_ID); + this->mark_failed(LOG_STR("Wrong chip ID")); + return; + } + + const uint8_t control = + (this->led_drive_ & 0b11) << CONTROL_PDRIVE_SHIFT | CONTROL_PDIODE_CH1 << CONTROL_PDIODE_SHIFT | + (this->proximity_gain_ & 0b11) << CONTROL_PGAIN_SHIFT | (this->ambient_gain_ & 0b11) << CONTROL_AGAIN_SHIFT; + + uint8_t enable = APDS9930_PON; + if (this->illuminance_sensor_ != nullptr) + enable |= APDS9930_AEN; + if (this->proximity_sensor_ != nullptr) + enable |= APDS9930_PEN; + + if (!this->write_reg_(APDS9930_ENABLE, 0x00) || !this->write_reg_(APDS9930_ATIME, APDS9930_ATIME_VALUE) || + !this->write_reg_(APDS9930_PTIME, APDS9930_PTIME_VALUE) || + !this->write_reg_(APDS9930_PPULSE, APDS9930_PPULSE_VALUE) || !this->write_reg_(APDS9930_CONTROL, control) || + !this->write_reg_(APDS9930_ENABLE, enable)) { + this->mark_failed(LOG_STR("Configuration failed")); + } +} + +void APDS9930Component::dump_config() { + ESP_LOGCONFIG(TAG, + "APDS9930:\n" + " LED Drive: %.1f mA\n" + " Proximity Gain: %ux\n" + " Ambient Light Gain: %ux", + LED_CURRENT_MAX_MA / (1 << this->led_drive_), + progmem_read_byte(&PROXIMITY_GAINS[this->proximity_gain_]), + progmem_read_byte(&AMBIENT_GAINS[this->ambient_gain_])); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_); + LOG_SENSOR(" ", "Proximity", this->proximity_sensor_); +} + +void APDS9930Component::update() { + // STATUS, CH0DATAL/H, CH1DATAL/H, PDATAL/H + uint8_t raw[7]; + if (!this->read_bytes(APDS9930_STATUS | APDS9930_CMD_AUTO_INCREMENT, raw, sizeof(raw))) { + this->status_set_warning(LOG_STR("Reading data failed")); + return; + } + this->status_clear_warning(); + const uint8_t status = raw[0]; + + if (this->illuminance_sensor_ != nullptr && (status & APDS9930_AVALID) != 0) { + const uint16_t ch0 = encode_uint16(raw[2], raw[1]); + const uint16_t ch1 = encode_uint16(raw[4], raw[3]); + if (ch0 >= ALS_SATURATION || ch1 >= ALS_SATURATION) { + // A saturated channel would give a lux value that is too low, not too high + ESP_LOGW(TAG, "Light channel saturated, lower ambient_light_gain"); + this->illuminance_sensor_->publish_state(NAN); + } else { + const float iac = std::max({ch0 - APDS9930_ALS_B * ch1, APDS9930_ALS_C * ch0 - APDS9930_ALS_D * ch1, 0.0f}); + this->illuminance_sensor_->publish_state(iac * LUX_PER_COUNT_1X / + progmem_read_byte(&AMBIENT_GAINS[this->ambient_gain_])); + } + } + + if (this->proximity_sensor_ != nullptr && (status & APDS9930_PVALID) != 0) { + this->proximity_sensor_->publish_state(encode_uint16(raw[6], raw[5])); + } +} + +} // namespace esphome::apds9930 diff --git a/esphome/components/apds9930/apds9930.h b/esphome/components/apds9930/apds9930.h new file mode 100644 index 0000000000..cc7f77d3d0 --- /dev/null +++ b/esphome/components/apds9930/apds9930.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +namespace esphome::apds9930 { + +class APDS9930Component final : public PollingComponent, public i2c::I2CDevice { + SUB_SENSOR(illuminance) + SUB_SENSOR(proximity) + + public: + void setup() override; + void dump_config() override; + void update() override; + + void set_led_drive(uint8_t level) { this->led_drive_ = level; } + void set_proximity_gain(uint8_t gain) { this->proximity_gain_ = gain; } + void set_ambient_gain(uint8_t gain) { this->ambient_gain_ = gain; } + + protected: + bool write_reg_(uint8_t reg, uint8_t value); + + uint8_t led_drive_{0}; + uint8_t proximity_gain_{0}; + uint8_t ambient_gain_{0}; +}; + +} // namespace esphome::apds9930 diff --git a/esphome/components/apds9930/sensor.py b/esphome/components/apds9930/sensor.py new file mode 100644 index 0000000000..2b169271c6 --- /dev/null +++ b/esphome/components/apds9930/sensor.py @@ -0,0 +1,70 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +from esphome.components.const import UNIT_COUNTS +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_ILLUMINANCE, + DEVICE_CLASS_ILLUMINANCE, + STATE_CLASS_MEASUREMENT, + UNIT_LUX, +) +from esphome.types import ConfigType + +DEPENDENCIES = ["i2c"] + +CONF_AMBIENT_LIGHT_GAIN = "ambient_light_gain" +CONF_LED_DRIVE = "led_drive" +CONF_PROXIMITY = "proximity" +CONF_PROXIMITY_GAIN = "proximity_gain" + +DRIVE_LEVELS = {"100ma": 0, "50ma": 1, "25ma": 2, "12.5ma": 3} +PROXIMITY_LEVELS = {"1x": 0, "2x": 1, "4x": 2, "8x": 3} +AMBIENT_LEVELS = {"1x": 0, "8x": 1, "16x": 2, "120x": 3} + +apds9930_ns = cg.esphome_ns.namespace("apds9930") +APDS9930Component = apds9930_ns.class_( + "APDS9930Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(APDS9930Component), + cv.Optional(CONF_LED_DRIVE, default="100ma"): cv.enum( + DRIVE_LEVELS, lower=True + ), + cv.Optional(CONF_PROXIMITY_GAIN, default="8x"): cv.enum( + PROXIMITY_LEVELS, lower=True + ), + cv.Optional(CONF_AMBIENT_LIGHT_GAIN, default="1x"): cv.enum( + AMBIENT_LEVELS, lower=True + ), + cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( + unit_of_measurement=UNIT_LUX, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PROXIMITY): sensor.sensor_schema( + unit_of_measurement=UNIT_COUNTS, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x39)) +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + cg.add(var.set_led_drive(config[CONF_LED_DRIVE])) + cg.add(var.set_proximity_gain(config[CONF_PROXIMITY_GAIN])) + cg.add(var.set_ambient_gain(config[CONF_AMBIENT_LIGHT_GAIN])) + sensors = sensor.sub_sensors(config) + await sensors(CONF_ILLUMINANCE, var.set_illuminance_sensor) + await sensors(CONF_PROXIMITY, var.set_proximity_sensor) diff --git a/tests/components/apds9930/common.yaml b/tests/components/apds9930/common.yaml new file mode 100644 index 0000000000..2cb9547e5c --- /dev/null +++ b/tests/components/apds9930/common.yaml @@ -0,0 +1,13 @@ +sensor: + - platform: apds9930 + i2c_id: i2c_bus + id: apds9930_sensor + address: 0x39 + update_interval: 30s + led_drive: 25ma + proximity_gain: 4x + ambient_light_gain: 16x + illuminance: + name: APDS9930 Illuminance + proximity: + name: APDS9930 Proximity diff --git a/tests/components/apds9930/test.esp32-idf.yaml b/tests/components/apds9930/test.esp32-idf.yaml new file mode 100644 index 0000000000..8226f36aa7 --- /dev/null +++ b/tests/components/apds9930/test.esp32-idf.yaml @@ -0,0 +1,3 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + apds9930: !include common.yaml diff --git a/tests/components/apds9930/test.esp8266-ard.yaml b/tests/components/apds9930/test.esp8266-ard.yaml new file mode 100644 index 0000000000..913a4adeea --- /dev/null +++ b/tests/components/apds9930/test.esp8266-ard.yaml @@ -0,0 +1,3 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + apds9930: !include common.yaml diff --git a/tests/components/apds9930/test.rp2040-ard.yaml b/tests/components/apds9930/test.rp2040-ard.yaml new file mode 100644 index 0000000000..be77d931df --- /dev/null +++ b/tests/components/apds9930/test.rp2040-ard.yaml @@ -0,0 +1,3 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + apds9930: !include common.yaml