Для разработки используются:
Плата 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
