Для разработки используются:
Плата NodeMCU ESP32

AP3216 (датчик света и приближения)

Резистор 330 Ом

Базовая настройка
Для разработки я использую Home Assistant с плагином ESPHome Device Builder, но можно использовать командную строку и ESPHome Device Builder отдельно от Home Assistant.
Для начала подключим ESP32 по usb к компьютеру и установим базовую прошивку через сайт web.esphome.io. После установки прошивки и ввода логина/пароля от wifi, устройство должно отобразиться в Home Assistant.

Теперь подключим датчик AP3216 к ESP32 по следующей схеме:

В реальности выглядит так

В корневой папке создаем следующие файлы:
main.yaml (основной файл),
папка components (здесь будут наши компоненты),
папка ap3216 (название компонента, внутри папки components),
файлы __init__.py, ap3216.h, ap3216.cpp, automation.h и sensor.py. Все файлы внутри папки ap3216. Название главного cpp и h файла должны совпадать с названием папки в которой они находятся.
Структура созданных папок и файлов:
<CONFIG_DIR>
├── main.yaml // основной yaml-файл
└── components // папка с внешними компонентами
└── ap3216 // папка с компонентом ap3216
├── __init__.py
├── ap3216.cpp
├── ap3216.h
├── automation.h
└── sensor.py
В yaml-файле (main.yaml) укажем внешний компонент ap3216:
esphome:
name: esphome-web-ebe1f0
friendly_name: ESPHome32
min_version: 2025.4.0
external_components:
- source:
type: local
path: components
components: [ap3216]
esp32:
board: esp32dev
framework:
type: arduino
# Enable Home Assistant API
api:
ota:
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: HIGH
web_server:
port: 80
logger:
level: VERBOSE
logs:
mqtt.component: DEBUG
mqtt.client: ERROR
Теперь можно приступить, непосредственно, к разработке компонента.
Создание простого компонента (MVP)
Простой компонент содержит сенсоры (датчики), которые считывают данные раз в n секунд и отображают их в GUI-версии.
В файле ap3216.h создадим класс компонента AP3216Component:
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/components/i2c/i2c.h"
#include "AP3216_WE.h"
#include "esphome/core/gpio.h"
namespace esphome {
namespace ap3216 {
class AP3216Component : public PollingComponent, public i2c::I2CDevice {
public:
void setup() override; //вызывается 1 раз при конфигурации компонента
void dump_config() override; //вызывается 1 раз после конфигурации компонента, в функции выводится информация о конфигурации компонента
void update() override; //вызывается раз в n времени, которое задается в yaml файле (update_interval)
void set_mode(AP3216Mode _ap3216_mode) { this->_ap3216_mode = _ap3216_mode; }
protected:
sensor::Sensor *ambient_light_sensor_{nullptr}; //обычный сенсор, поддерживает данные: int/float/double
sensor::Sensor *proximity_counts_sensor_{nullptr};
sensor::Sensor *infrared_counts_sensor_{nullptr};
binary_sensor::BinarySensor *is_near_sensor_{nullptr}; // бинарный сенсор: true/false
text_sensor::TextSensor *interrupt_status_sensor_{nullptr}; //текстовый сенсор, поддерживает текст
sensor::Sensor *ir_data_sensor_{nullptr};
AP3216Mode _ap3216_mode; // режим работы, есть три: считывание датчика приближения, считывание датчика освещения, считывания датчиков приближения и освещения
};
} // namespace ap3216
} // namespace esphome
В файле ap3216.cpp реализуем функции:
#include "ap3216.h"
#include "Wire.h"
#include "esphome/core/log.h"
namespace esphome {
namespace ap3216 {
static const char *const TAG = "ap3216.sensor";
AP3216_WE _AP3216 = AP3216_WE();
void AP3216Component::setup(){
_AP3216.setMode(_ap3216_mode);
delay(1000);
}
void AP3216Component::dump_config() {
ESP_LOGCONFIG(TAG, "_AP3216 init");
ESP_LOGCONFIG(TAG, "mode: %d ", this->_ap3216_mode);
}
void AP3216Component::update() {
float als = _AP3216.getAmbientLight();
unsigned int prox = _AP3216.getProximity();
unsigned int intStatus = _AP3216.getIntStatus();
unsigned int ir = _AP3216.getIRData(); // Ambient IR light
bool isNear = _AP3216.objectIsNear();
bool irIsValid = !_AP3216.irDataIsOverflowed();
if (this->ambient_light_sensor_ != nullptr) { //если параметр не указан в yaml-файле, то он будет иметь значения nullptr
this->ambient_light_sensor_->publish_state(als);
}
if (this->proximity_counts_sensor_ != nullptr) {
this->proximity_counts_sensor_->publish_state(prox);
}
if (this->infrared_counts_sensor_ != nullptr) {
this->infrared_counts_sensor_->publish_state(ir);
}
if (this->is_near_sensor_ != nullptr){
this->is_near_sensor_->publish_state(isNear);
}
if (this->ir_data_sensor_ != nullptr){
this->ir_data_sensor_->publish_state(_AP3216.getIRData());
}
if (this->interrupt_status_sensor_ != nullptr){
switch (intStatus)
{
case 0:
this->interrupt_status_sensor_->publish_state("NO_INT");
break;
case 1:
this->interrupt_status_sensor_->publish_state("ALS_INT");
break;
case 2:
this->interrupt_status_sensor_->publish_state("PS_INT");
break;
case 3:
this->interrupt_status_sensor_->publish_state("ALS_PS_INT");
break;
}
}
}
}
}
В файле sensor.py опишем конфигурацию:
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import i2c, sensor, binary_sensor, text_sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_AMBIENT_LIGHT,
CONF_ID,
CONF_NAME,
CONF_TRIGGER_ID,
DEVICE_CLASS_DISTANCE,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_PRESENCE,
ICON_BRIGHTNESS_5,
ICON_BRIGHTNESS_6,
ICON_MOTION_SENSOR,
ICON_SCREEN_ROTATION,
STATE_CLASS_MEASUREMENT,
UNIT_LUX,
)
DEPENDENCIES = ["i2c"] '''зависимость от i2c, в yaml-файле надо будет создать компонент i2c и указать пины для него (sda/scl)'''
AUTO_LOAD = ["sensor", "binary_sensor", "text_sensor"] '''список автоматически загружаемых компонентов (сенсоров), можно загружать кнопки, дисплеи или любые другие компоненты'''
REQUIRED_LIBRARIES = [
"Wire",
] '''Необходимые библиотеки, esphome загрузит автоматически во время сборки'''
CONF_INFRARED_COUNTS = "infrared_counts"
CONF_PS_COUNTS = "ps_counts"
CONF_IS_NEAR = "is_near"
UNIT_COUNTS = "#"
ICON_PROXIMITY = "mdi:hand-wave-outline"
CONF_MODE="mode"
CONF_OPERATING_MODE = "operating_mode"
CONF_INT_STATUS="interrupt_status"
CONF_IR_DATA="ir_data"
CONF_LUX_RANGE="lux_range"
ap3216_ns = cg.esphome_ns.namespace("ap3216")
AP3216Component = ap3216_ns.class_(
"AP3216Component", cg.PollingComponent, i2c.I2CDevice
)
AP3216MODE = cg.global_ns.enum("AP3216Mode")
MODE_OPTIONS = {
"ALS": AP3216MODE.AP3216_ALS,
"PS": AP3216MODE.AP3216_PS,
"ALS_PS": AP3216MODE.AP3216_ALS_PS,
"RESET": AP3216MODE.AP3216_RESET,
}
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(AP3216Component),
cv.Optional(CONF_INT_STATUS): cv.maybe_simple_value(
text_sensor.text_sensor_schema(icon=ICON_SCREEN_ROTATION),
key=CONF_NAME,
),
cv.Optional(CONF_IR_DATA): cv.maybe_simple_value(
sensor.sensor_schema(
icon=ICON_BRIGHTNESS_6,
accuracy_decimals=1,
device_class=DEVICE_CLASS_ILLUMINANCE,
),
key=CONF_NAME,
),
cv.Optional(CONF_PS_COUNTS): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_COUNTS,
icon=ICON_PROXIMITY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_INFRARED_COUNTS): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_COUNTS,
icon=ICON_BRIGHTNESS_5,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
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_IS_NEAR): cv.maybe_simple_value(
binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_PRESENCE,
icon=ICON_MOTION_SENSOR,
),
key=CONF_NAME,
),
cv.Optional(CONF_MODE, default="ALS_PS"): cv.enum(MODE_OPTIONS),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x23)),
) '''основная схема компонента, тут описываются параметры, триггеры, ограничения'''
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add_library("AP3216_WE", None, "https://github.com/wollewald/AP3216_WE.git") '''загрузка сторонней библиотеки'''
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if int_status_config := config.get(CONF_INT_STATUS):
sens = await text_sensor.new_text_sensor(int_status_config)
cg.add(var.set_interrupt_status_sensor(sens))
if ir_data_config := config.get(CONF_IR_DATA):
sens = await sensor.new_sensor(ir_data_config)
cg.add(var.set_ir_data_sensor(sens))
if als_config := config.get(CONF_AMBIENT_LIGHT):
sens = await sensor.new_sensor(als_config)
cg.add(var.set_ambient_light_sensor(sens))
if prox_cnt_config := config.get(CONF_PS_COUNTS):
sens = await sensor.new_sensor(prox_cnt_config)
cg.add(var.set_proximity_counts_sensor(sens))
if infrared_cnt_config := config.get(CONF_INFRARED_COUNTS):
sens = await sensor.new_sensor(infrared_cnt_config)
cg.add(var.set_infrared_counts_sensor(sens))
if is_near_config := config.get(CONF_IS_NEAR):
sens = await binary_sensor.new_binary_sensor(is_near_config)
cg.add(var.set_is_near_sensor(sens))
cg.add(var.set_mode(config[CONF_MODE]))
Теперь можно добавить наш сенсор, в созданный ранее yaml файл (main.yaml):
...
i2c:
sda: GPIO21
scl: GPIO22
sensor:
- platform: ap3216
ambient_light: "Ambient light"
ps_counts: "Proximity"
infrared_counts: "Infrared"
ir_data: "Ir data"
is_near: "is_near"
address: 0x23
update_interval: 60s
...
После установки прошивки, при открытии адреса датчика, будет отображаться таблица с данными (которые будут обновляться раз в 60 секунд):

Вместе с логами так

Триггеры
Триггеры позволяют создавать дополнительную логику в yaml-файлах, которая будет срабатывать при наступлении определенных событий. Например, можно отслеживать находится ли какой-то объект в непосредственной близости от датчика приближения или нет.
Создадим два триггера: on_ps_low_threshold и on_ps_high_threshold. Первый триггер будет срабатывать, когда какой-то предмет находится в непосредственной близости от датчика, второй - когда предмета не обнаружено.
В файл ap3216.h добавим Callback'и:
CallbackManager<void()> on_ps_high_trigger_callback_;
CallbackManager<void()> on_ps_low_trigger_callback_;
void add_on_ps_high_trigger_callback_(std::function<void()> callback) {
this->on_ps_high_trigger_callback_.add(std::move(callback));
}
void add_on_ps_low_trigger_callback_(std::function<void()> callback) {
this->on_ps_low_trigger_callback_.add(std::move(callback));
}
В файле с автоматизацией automation.h создадим классы AP3216PsHighTrigger и AP3216PsLowTrigger, унаследованные от Trigger<>:
#pragma once
#include "esphome/core/automation.h"
#include "ap3216.h"
namespace esphome {
namespace ap3216 {
class AP3216PsHighTrigger : public Trigger<> {
friend class AP3216Component;
public:
explicit AP3216PsHighTrigger(AP3216Component *parent) {
parent->add_on_ps_high_trigger_callback_([this]() { this->trigger(); });
}
};
class AP3216PsLowTrigger : public Trigger<> {
public:
explicit AP3216PsLowTrigger(AP3216Component *parent) {
parent->add_on_ps_low_trigger_callback_([this]() { this->trigger(); });
}
};
В файле ap3216.cpp вызовем триггер при обновлении данных с датчиков:
void AP3216Component::update() {
...
bool isNear = _AP3216.objectIsNear();
...
if (isNear){
this->on_ps_high_trigger_callback_.call();
}else{
this->on_ps_low_trigger_callback_.call();
}
...
}
В файл sensor.py добавим триггеры:
...
from esphome.const import (
...
CONF_TRIGGER_ID,
...
)
CONF_ON_PS_HIGH_THRESHOLD = "on_ps_high_threshold"
CONF_ON_PS_LOW_THRESHOLD = "on_ps_low_threshold"
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
...
cv.Optional(CONF_ON_PS_HIGH_THRESHOLD): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(AP3216PsHighTrigger),
}
),
cv.Optional(CONF_ON_PS_LOW_THRESHOLD): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(AP3216PsLowTrigger),
}
),
...
}
)
...
)
...
async def to_code(config):
...
for prox_high_tr in config.get(CONF_ON_PS_HIGH_THRESHOLD, []):
trigger = cg.new_Pvariable(prox_high_tr[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], prox_high_tr)
for prox_low_tr in config.get(CONF_ON_PS_LOW_THRESHOLD, []):
trigger = cg.new_Pvariable(prox_low_tr[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], prox_low_tr)
...
Теперь в yaml файл можно добавить триггеры on_ps_low_threshold и on_ps_high_threshold:
sensor:
- platform: ap3216
mode: ALS_PS
ps_counts: "Proximity"
infrared_counts: "Infrared"
ir_data: "Ir data"
is_near: "is_near"
on_ps_low_threshold:
then:
- logger.log: "Object is not near"
on_ps_high_threshold:
then:
- logger.log: "Object is near"
Пример срабатывания триггера:

Проверка дополнительных условий
В файле sensor.py (при конфигурировании схемы - CONFIG_SCHEMA) можно осуществить проверку дополнительных условий, например, задать интервал валидных значений:
cv.Optional(CONF_PS_CALIBRATION, default=0): cv.int_range(min=0, max=511),
или выбор из диапазона значений:
cv.Optional(CONF_PS_INT_AFTER_N_CONVERSIONS): cv.one_of(1, 2, 4, 8, int=True),
или выбор из перечисления:
cv.Optional(CONF_MODE, default="ALS_PS"): cv.enum(MODE_OPTIONS),
Для более сложных проверок можно использовать отдельные функции, например, для проверки, что два взаимосвязанных параметра либо оба заданы, либо оба не заданы.
def validate_thresholds(config):
has_als_lower_thresh = CONF_ALS_THRESHOLDS_LOWER in config
has_als_upper_thresh = CONF_ALS_THRESHOLDS_UPPER in config
if has_als_lower_thresh != has_als_upper_thresh:
raise cv.Invalid("als_lower_thresh and als_upper_thresh must be both set or both not set")
return config
Проверку отдельной функцией необходимо включить в схему:
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
...
}
)
validate_thresholds
)
Теперь, на этапе компиляции, будут произведены дополнительные проверки yaml файла. Если значения параметров не соответствуют критериям - будет выведена ошибка.
Пример ошибки, при неверном задании mode в yaml-файле:

Прерывания
Прерывания позволяют получать уведомления от внешних компонентов, непосредственно, при наступлении тех или иных событий, не дожидаясь обновления данных по таймеру. В нашем случае такими событиями могут быть: срабатывания датчика освещения или приближения, в том случае, если значения этих датчиков попадают в заданный интервал (интервалы задаются в yaml-файле).
Для начала создадим триггер on_interrupt_trigger аналогично, созданным ранее триггерам, но добавив передачу данных через него (текущие значения датчиков и тип прерывания). Данные будут доступны в yaml-файле при срабатывании триггера.
В файл ap3216.h добавим callback для триггера и pin, через который будет срабатывать прерывание :
namespace esphome {
namespace ap3216 {
struct AP3216Data{
float als;
unsigned int prox;
unsigned int ir;
uint8_t interruptType;
std::string interruptTypeString;
};
class AP3216Component : public PollingComponent, public i2c::I2CDevice {
public:
CallbackManager<void(AP3216Data &)> on_interrupt_callback_;
void add_on_interrupt_callback_(std::function<void(AP3216Data &)> &&callback){
this->on_interrupt_callback_.add(std::move(callback));
}
void set_interrupt_pin(InternalGPIOPin *pin) { interrupt_pin_ = pin; }
protected:
InternalGPIOPin *interrupt_pin_{nullptr};
};
} // namespace ap3216
} // namespace esphome
В файл automation.h добавим триггер с указанием на класс компонента AP3216Data:
#pragma once
#include "esphome/core/automation.h"
#include "ap3216.h"
namespace esphome {
namespace ap3216 {
...
class AP3216InterruptTrigger : public Trigger<AP3216Data&>
{
friend class AP3216Component;
public:
explicit AP3216InterruptTrigger(AP3216Component *parent)
{
parent->add_on_interrupt_callback_([this](AP3216Data &x) { this->trigger(x); });
}
};
} // namespace ap3216
} // namespace esphome
В файле ap3216.cpp подключим прерывания:
void AP3216Component::setup(){
...
if (operating_mode == 1 || operating_mode == 2){
int pin = interrupt_pin_->get_pin();
pinMode(pin, INPUT); //подключения pin для отслеживания прерываний
attachInterrupt(digitalPinToInterrupt(pin), blink, CHANGE); //создание прерывания
/*настройка дополнительных параметров: */
_AP3216.setALSThresholds(als_lower_thresh, als_upper_thresh);
_AP3216.setPSThresholds(ps_lower_thresh, ps_upper_thresh);
_AP3216.setPSInterruptMode(ps_interrupt_mode);
_AP3216.setPSIntAfterNConversions(ps_int_after_n_conversions);
_AP3216.setALSIntAfterNConversions(als_int_after_n_conversions);
_AP3216.setIntClearManner(int_clear_manner);
}
void AP3216Component::interruptAction(){
ESP_LOGI(TAG, "interruptAction ... OK!");
uint8_t intType = NO_INT;
intType = _AP3216.getIntStatus();
AP3216Data data;
data.interruptType = intType;
switch(intType){
case(ALS_INT):
ESP_LOGI(TAG, "Ambient Light Interrupt!");
data.interruptTypeString = "ALS_INT";
data.als = _AP3216.getAmbientLight();
data.prox = _AP3216.getProximity();
data.ir = _AP3216.getIRData();
this->on_interrupt_callback_.call(data);
break;
case(PS_INT):
data.interruptTypeString = "PS_INT";
ESP_LOGI(TAG, "Proximity Interrupt!");
data.als = _AP3216.getAmbientLight();
data.prox = _AP3216.getProximity();
data.ir = _AP3216.getIRData();
this->on_interrupt_callback_.call(data);
break;
case(ALS_PS_INT):
ESP_LOGI(TAG, "Ambient Light and Proximity Interrupt!");
data.interruptTypeString = "ALS_PS_INT";
data.als = _AP3216.getAmbientLight();
data.prox = _AP3216.getProximity();
data.ir = _AP3216.getIRData();
this->on_interrupt_callback_.call(data);
break;
default:
ESP_LOGI(TAG, "Something went wrong...");
break;
}
intType = _AP3216.getIntStatus();
_AP3216.clearInterrupt(intType);
event = false;
}
void AP3216Component::loop(){
if(event){
interruptAction();
}
/*
* without the following delay you will not detect ALS and PS interrupts together.
*/
delay(1000);
}
void AP3216Component::blink(){
event = true;
}
}
Добавим триггер в файл sensor.py:
from esphome import automation, pins
...
CONF_INTERRUPT_PIN = "interrupt_pin"
AP3216InterruptTrigger = ap3216_ns.class_('AP3216InterruptTrigger', automation.Trigger.template())
CONF_ON_INTERRUPT_TRIGGER = "on_interrupt_trigger"
...
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_ON_INTERRUPT_TRIGGER): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(AP3216InterruptTrigger),
},
),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
}
)
...
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
...
if CONF_INTERRUPT_PIN in config:
interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN])
cg.add(var.set_interrupt_pin(interrupt_pin))
for conf in config.get(CONF_ON_INTERRUPT_TRIGGER, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(AP3216Data.operator("ref"), "x")],
conf,
)
Теперь триггер можно подключить в yaml-файле:
sensor:
- platform: ap3216
mode: ALS_PS
operating_mode: VALUE_AND_INTERRUPT
interrupt_pin: GPIO13
int_clear_manner: CLR_INT_MANUALLY
interrupt_status: "status"
ambient_light: "Ambient light"
ps_counts: "Proximity"
infrared_counts: "Infrared"
ir_data: "Ir data"
is_near: "is_near"
on_interrupt_trigger:
- if:
condition:
lambda: return x.interruptTypeString == "ALS_PS_INT";
then:
- logger.log:
level: INFO
format: "als: %f, prox: %d, ir: %d, interruptTypeString: %s"
args:
- x.als
- x.prox
- x.ir
- x.interruptTypeString.c_str()
address: 0x23
update_interval: 60s
При срабатывании прерывания будет выведена информация в лог:

Выводы
После публикации компонента в интернете (github, gitlab и т.д.), его можно подключать в любых yaml файлах.
Для подключения достаточно указать ссылку на внешний компонент:
external_components:
- source: github://10-thousand/esphome-AP3216@main
components: [ap3216]
А затем подключить сам компонент:
sensor:
- platform: ap3216
mode: ALS_PS
operating_mode: VALUE
ambient_light: "Ambient light"
ps_counts: "Proximity"
infrared_counts: "Infrared"
ir_data: "Ir data"
is_near: "is_near"
address: 0x23
update_interval: 60s
Пример yaml файла после подключения внешнего компонента:
esphome:
name: esphome-web-ebe1f0
friendly_name: ESPHome32
min_version: 2025.4.0
external_components:
- source: github://10-thousand/esphome-AP3216@main
components: [ap3216]
esp32:
board: esp32dev
framework:
type: arduino
# Enable Home Assistant API
api:
ota:
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: HIGH
web_server:
port: 80
i2c:
sda: GPIO21
scl: GPIO22
sensor:
- platform: ap3216
mode: ALS_PS
operating_mode: VALUE
ambient_light: "Ambient light"
ps_counts: "Proximity"
infrared_counts: "Infrared"
ir_data: "Ir data"
is_near: "is_near"
address: 0x23
update_interval: 60s
logger:
level: VERBOSE
Тут yaml-файл со всеми возможными опциями
esphome:
name: esphome-web-ebe1f0
friendly_name: ESPHome32
min_version: 2025.4.0
external_components:
#- source:
# type: local
# path: components
#components: [ap3216]
- source: github://10-thousand/esphome-AP3216@main
components: [ap3216]
esp32:
board: esp32dev
framework:
type: arduino
# Enable Home Assistant API
api:
ota:
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: HIGH
web_server:
port: 80
i2c:
sda: GPIO21
scl: GPIO22
sensor:
- platform: ap3216
mode: ALS_PS
operating_mode: VALUE_AND_INTERRUPT
interrupt_pin: GPIO13
int_clear_manner: CLR_INT_MANUALLY
interrupt_status: "status"
ambient_light: "Ambient light"
ps_counts: "Proximity"
infrared_counts: "Infrared"
ir_data: "Ir data"
is_near: "is_near"
lux_range: RANGE_20661
als_int_after_n_conversions: 6
ps_int_after_n_conversions: 2
als_calibration_factor: 1.0
ps_integration_time: 8
ps_gain: 2
ps_interrupt_mode: INT_MODE_ZONE
ps_mean_time: PS_MEAN_TIME_12_5
number_of_led_pulses: 1
led_current: LED_66_7
led_waiting_time: 0
ps_calibration: 0
als_thresholds_lower: 0
als_thresholds_upper: 500
ps_thresholds_lower: 0
ps_thresholds_upper: 200
on_interrupt_trigger:
- if:
condition:
lambda: return x.interruptTypeString == "ALS_PS_INT";
then:
- logger.log:
level: INFO
format: "als: %f, prox: %d, ir: %d, interruptTypeString: %s"
args:
- x.als
- x.prox
- x.ir
- x.interruptTypeString.c_str()
on_ps_low_threshold:
then:
- logger.log: "Object is not near"
on_ps_high_threshold:
then:
- logger.log: "Object is near"
on_ir_data_overflow:
then:
- logger.log: "ir data is overflowed"
address: 0x23
update_interval: 60s
logger:
level: VERBOSE
logs:
mqtt.component: DEBUG
mqtt.client: ERROR
Теперь веб-страница устройства выглядит так:

Полезные ссылки
ESPHome custom component for СJMCU-3216 (AP3216)
ESP Home
Home Assistant
Исходный код стандартных компонентов esphome
Библиотека AP3216
Документация для модуля AP3216