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

Плата 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

Комментарии (0)