[apds9930] Add ambient light and proximity sensor (#13125)

Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Ilya Esin
2026-10-12 13:14:31 -10:00
committed by GitHub
co-authored by J. Nick Koston
parent 132e9dac8b
commit fa519cea64
9 changed files with 265 additions and 0 deletions
+1
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
CODEOWNERS = ["@iyesin"]
DOMAIN = "apds9930"
+140
View File
@@ -0,0 +1,140 @@
#include "apds9930.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <cmath>
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
+30
View File
@@ -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
+70
View File
@@ -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)
+13
View File
@@ -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
@@ -0,0 +1,3 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
apds9930: !include common.yaml
@@ -0,0 +1,3 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
apds9930: !include common.yaml
@@ -0,0 +1,3 @@
packages:
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
apds9930: !include common.yaml