ESPHome外部组件(external components)的文件结构:

例如现在要写mmwave kit 的 seeed_mr24hpc1 的外部组件,那么文件目录初步规划至少是:
-floder
-components
-seeed_mr24hpc1
init.py
seeed_mr24hpc1.cpp
seeed_mr24hpc1.h
....
-exmaple.yaml
这些文件该怎么写,没有什么好办法,基本上就是参考esphome类似传感器的现有的代码:
esphome/esphome/components/ld2410 at dev · limengdu/esphome (github.com)
程序中用到的Python的库和工具,都需要自行查阅ESPHome仓库下面的core文件夹:
from esphome.automation import maybe_simple_id
# 第三方库的源代码路径:esphome/core/automation.hESPHome提供了一些空白的示例:
esphome-external-component-examples/custom_components at master · jesserockz/esphome-external-component-examples (github.com)
我就从空白示例开始吧。
// mr24hpc1.cpp
#include "esphome/core/log.h"
#include "mr24hpc1.h"
namespace esphome {
namespace mr24hpc1_text_sensor {
static const char *TAG = "mr24hpc1_text_sensor.text_sensor";
void mr24hpc1TextSensor::setup() {
}
void mr24hpc1TextSensor::dump_config() {
ESP_LOGCONFIG(TAG, "Empty text sensor");
}
} // namespace empty_text_sensor
} // namespace esphome// mr24hpc1.h
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/text_sensor/text_sensor.h"
namespace esphome {
namespace mr24hpc1_text_sensor {
class mr24hpc1TextSensor : public text_sensor::TextSensor, public Component { // 类名必须是text_sensor.py定义的名字
public:
void setup() override;
void dump_config() override;
};
} // namespace empty_text_sensor
} // namespace esphome# text_sensor.py
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import text_sensor
from esphome.const import CONF_ID
CODEOWNERS = ["@limengdu"]
# 这行代码创建了一个新的名为 mr24hpc1_ns 的命名空间。
# 该命名空间将作为 mr24hpc1_ns 组件相关的所有类、函数和变量的前缀,确保它们不会与其他组件的名称产生冲突。
mr24hpc1_text_sensor_ns = cg.esphome_ns.namespace('mr24hpc1_text_sensor')
# 这个 MyCustomTextSensor 类将是一个定期轮询的 UART 设备
mr24hpc1TextSensor = mr24hpc1_text_sensor_ns.class_('mr24hpc1TextSensor', text_sensor.TextSensor, cg.Component)
# sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 1) 创建了一个基础的传感器模式,设置了单位 (UNIT_EMPTY),图标 (ICON_EMPTY),以及数据的小数点位数(1)。
# .extend({ cv.GenerateID(): cv.declare_id(mr24hpc1Sensor), }) 扩展了基础模式,添加了一个必需的 ID。
# cv.GenerateID() 是一个函数,它生成一个唯一的 ID,cv.declare_id(mr24hpc1Sensor) 声明了一个新的 mr24hpc1Sensor ID。
# .extend(cv.polling_component_schema('60s')) '60s' 指定了默认的轮询间隔为 60 秒。
# .extend(uart.UART_DEVICE_SCHEMA) 这允许用户在配置文件中设置 UART 设备的参数,如波特率和接收/发送引脚。
CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(mr24hpc1TextSensor)
}).extend(cv.COMPONENT_SCHEMA)
def to_code(config):
# 这行代码创建了一个新的 Pvariable(一个代表 C++ 变量的 Python 对象),变量的 ID 是从配置中取出的。
var = cg.new_Pvariable(config[CONF_ID])
# 注册了一个文本传感器
yield text_sensor.register_text_sensor(var, config)
# 这个生成器负责生成注册组件所需要的 C++ 代码。
yield cg.register_component(var, config)可见这个空白示例我是选择了一个text_sensor的模板,示例的yaml文件如下:
substitutions:
name: "seeedstudio-mmwave-kit"
friendly_name: "SeeedStudio mmWave Kit"
esphome:
name: "${name}"
friendly_name: "${friendly_name}"
name_add_mac_suffix: true
project:
name: "seeedstudio.mmwave_kit"
version: "1.2"
platformio_options:
board_build.flash_mode: dio
board_build.mcu: esp32c3
external_components:
- source: github://limengdu/mmwave-kit-external-components@main
refresh: 0s
esp32:
board: esp32-c3-devkitm-1
variant: esp32c3
framework:
type: esp-idf
# Enable logging
logger:
hardware_uart: USB_SERIAL_JTAG
level: DEBUG
# Enable Home Assistant API
api:
ota:
wifi:
ssid: "mengdu-H68K"
password: "15935700"
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "mmwave-kit"
captive_portal:
# Sets up Bluetooth LE (Only on ESP32) to allow the user
# to provision wifi credentials to the device.
esp32_improv:
authorizer: none
# Sets up the improv via serial client for Wi-Fi provisioning.
# Handy if your device has a usb port for the user to add credentials when they first get it.
# improv_serial: # Commented until improv works with usb-jtag on idf
uart:
id: uart_bus
baud_rate: 115200
rx_pin: 4
tx_pin: 5
text_sensor:
- platform: seeed_mr24hpc1
name: Empty text sensor有一些地方是需要注意的:
yaml文件中的platform就是你的components文件夹中,组件的文件夹名称,否则会报错。
驱动文件的命名空间和类名必须要和py文件的命名空间和类名一致。
mr24hpc1_text_sensor_ns = cg.esphome_ns.namespace('mr24hpc1_text_sensor')
mr24hpc1TextSensor = mr24hpc1_text_sensor_ns.class_('mr24hpc1TextSensor', text_sensor.TextSensor, cg.Component)这里,命名空间为:mr24hpc1_text_sensor,类名为:mr24hpc1TextSensor
继续读ld2410的代码,发现它们的代码基本上是按照不同的组件类型去编写py文件的,但是驱动文件却只有一组。所以捋清楚驱动文件和各个py文件之间的关系很重要。我将会一点点来。
驱动文件和各个py文件的联系构建
在原来的雷达程序中,我们是通过yaml文件里面给组件命名的id号来建立组件和驱动文件的联系的:
- platform: template
name: "Motion Trigger Boundary Setting"
id: custom_motion_trigger_boundary然后更新传感器数据的时候,直接就是:
id(custom_motion_trigger_boundary).publish_state(s_motion_trig_boundary_str[data[FRAME_DATA_INDEX] - 1]);这样,ESPHome就知道什么时候应该更新传感器的数值到什么地方去。
但是在阅读ld2010的驱动程序时,我发现并不是这样。它们的联系比较复杂,大致是以下几段代码块:
# sensor.py
CONF_MOVING_DISTANCE = "moving_distance"
if moving_distance_config := config.get(CONF_MOVING_DISTANCE):
sens = await sensor.new_sensor(moving_distance_config)
cg.add(ld2410_component.set_moving_target_distance_sensor(sens))// ld2410.h
SUB_SENSOR(moving_target_distance)// ld2410.cpp
if (this->moving_target_distance_sensor_ != nullptr) {
int new_moving_target_distance = this->two_byte_to_int_(buffer[MOVING_TARGET_LOW], buffer[MOVING_TARGET_HIGH]);
if (this->moving_target_distance_sensor_->get_state() != new_moving_target_distance)
this->moving_target_distance_sensor_->publish_state(new_moving_target_distance);
}# yaml
- platform: ld2410
moving_distance:
name : Moving Distance显然了,在cpp驱动中,传感器的数值会更新到变量moving_target_distance_sensor_中,而moving_target_distance_sensor_的值应该对应传到名为Moving Distance的ESPHome组件上。但是它们的变量名完全不同,这是咋做到的??
关键就在ld2410.h中SUB_SENSOR宏定义。这个宏定义的封装在esphome/components/sensor/sensor.h中:
#define SUB_SENSOR(name) \
protected: \
sensor::Sensor *name##_sensor_{nullptr}; \
\
public: \
void set_##name##_sensor(sensor::Sensor *sensor) { this->name##_sensor_ = sensor; }
// SUB_SENSOR的作用:例 SUB_SENSOR(temperature)
// protected:
// sensor::Sensor *temperature_sensor_{nullptr}; //sensor::Sensor *name##sensor{nullptr};
// public:
// void set_temperature_sensor(sensor::Sensor *sensor) { this->temperature_sensor_ = sensor; } //void set_##name##_sensor(sensor::Sensor *sensor) { this->name##sensor = sensor; }起的就是拼接的作用:moving_target_distance摇身一变就成了moving_target_distance_sensor_和set_moving_target_distance_sensor
SUB_SENSOR(moving_target_distance)
==
protected:
sensor::Sensor *moving_target_distance_sensor_{nullptr};
public:
void set_moving_target_distance_sensor(sensor::Sensor *sensor) { this->moving_target_distance_sensor_ = sensor; }moving_target_distance_sensor_就是传感器数值
set_moving_target_distance_sensor用在py文件里面的
一组驱动文件是如何为各种不同的传感器类型服务的
在头文件中,有非常多的这样的内容:
#ifdef USE_SENSOR
SUB_SENSOR(moving_target_distance)
SUB_SENSOR(still_target_distance)
SUB_SENSOR(moving_target_energy)
SUB_SENSOR(still_target_energy)
SUB_SENSOR(light)
SUB_SENSOR(detection_distance)
#endif
#ifdef USE_BINARY_SENSOR
SUB_BINARY_SENSOR(target)
SUB_BINARY_SENSOR(moving_target)
SUB_BINARY_SENSOR(still_target)
SUB_BINARY_SENSOR(out_pin_presence_status)
#endif
#ifdef USE_TEXT_SENSOR
SUB_TEXT_SENSOR(version)
SUB_TEXT_SENSOR(mac)
#endif
#ifdef USE_SELECT
SUB_SELECT(distance_resolution)
SUB_SELECT(baud_rate)
SUB_SELECT(light_function)
SUB_SELECT(out_pin_level)
#endif
#ifdef USE_SWITCH
SUB_SWITCH(engineering_mode)
SUB_SWITCH(bluetooth)
#endif
#ifdef USE_BUTTON
SUB_BUTTON(reset)
SUB_BUTTON(restart)
SUB_BUTTON(query)
#endif
#ifdef USE_NUMBER
SUB_NUMBER(max_still_distance_gate)
SUB_NUMBER(max_move_distance_gate)
SUB_NUMBER(timeout)
SUB_NUMBER(light_threshold)
#endif所以我感觉,USE_SENSOR、USE_BINARY_SENSOR 等宏可能被用来控制是否包含与特定类型的传感器相关的代码。例如,如果你不需要二进制传感器的功能,你就可以不定义 USE_BINARY_SENSOR,这样与二进制传感器相关的代码就不会被编译进你的程序中。
以及,不同的传感器类型应当有对应的py文件来生成组件,例如在ld2410雷达中,就有text_sensor.py、sensor.py等等。
捋清了这些关键问题之后,我觉得我就可以开始从雷达的一些固定值开始写起了。例如固件、版本等等。
text_sensor
//mr24hpc1.h
#ifdef USE_TEXT_SENSOR
SUB_TEXT_SENSOR(heartbeat_state)
// heartbeat_state_text_sensor_ set_heartbeat_state_text_sensor
SUB_TEXT_SENSOR(product_model)
// product_model_text_sensor_ set_product_model_text_sensor
SUB_TEXT_SENSOR(product_id)
// product_id_text_sensor_ set_product_id_text_sensor
SUB_TEXT_SENSOR(hardware_model)
// hardware_model_text_sensor_ set_hardware_model_text_sensor
SUB_TEXT_SENSOR(firware_version)
// firware_version_text_sensor_ set_firware_version_text_sensor
#endif
SUB_TEXT_SENSOR(keep_away)
// keep_away_text_sensor_ set_keep_away_text_sensor
SUB_TEXT_SENSOR(motion_status)
// motion_status_text_sensor_ set_motion_status_text_sensor
SUB_TEXT_SENSOR(someoneExists)
// someoneExists_text_sensor_ set_someoneExists_text_sensor
SUB_TEXT_SENSOR(custom_presence_of_detection)
// custom_presence_of_detection_text_sensor_ set_custom_presence_of_detection_text_sensortext_sensor_schema() 支持的类型
entity_category,除此以外啥都不支持...
binary_sensor
//mr24hpc1.h
SUB_BINARY_SENSOR(someoneExists)
// someoneExists_binary_sensor set_someoneExists_binary_sensor碎碎念
# __init__.py
mr24hpc1Component = mr24hpc1_ns.class_("mr24hpc1Component", cg.PollingComponent, uart.UARTDevice)PollingComponent是包含loop、setup、update和dump_config这些函数的。
之后,在驱动的cpp文件中我们就可以使用这些函数,在头文件中可以这样去定义类:
// .h
class mr24hpc1Component : public PollingComponent, public uart::UARTDevice {}ESPHome的PollingComponent类提供了一个方便的方法来定期调用update()函数。当你创建一个PollingComponent时,你可以指定一个更新间隔(以毫秒为单位),然后ESPHome将在指定的间隔后调用该组件的update()函数。
// .h
public:
// constructor
MyCustomTextSensor() : PollingComponent(8000) {}以上就是8秒执行一次update函数,默认情况下是60秒执行一次。
在py文件中的这些后面引号的内容,就是yaml文件中的组件名称:
并且,前面的大写必须对应后面的小写,包括下划线,ESPHome称为键值对对称规则。
CONF_HEART_BEAT = "heart_beat"
CONF_PRODUCT_MODEL = "product_model"
CONF_PRODUCT_ID = "product_id"
CONF_HARDWARE_MODEL = "hardware_model"
CONF_HARDWARE_VERSION = "hardware_version"yaml:
text_sensor:
- platform: seeed_mr24hpc1
heart_beat:
name: "Heartbeat"
product_model:
name: "Product Model"
product_id:
name: "Product ID"
hardware_model:
name: "Hardware Model"
hardware_version:
name: "Hardware Version"components文件夹下面的名称就是esphome yaml文件的platform的名称。esphome官方要求这个名字需要和驱动包的名字和命名空间的类名相同!



关于ESPHome的c++代码格式问题,官方是使用clang-format进行检查的,可以通过使用VSCode的C++插件运行此项检查和修改。
您可以使用“设置文档格式”(Shift+Alt+F) 设置整个文件的格式,也可以使用右键单击上下文菜单中的“设置所选内容格式”(Ctrl+K、Ctrl+F) 仅设置当前选择的格式。

默认情况下,clang 格式样式设置为“file”,这意味着它会在工作区中查找 .clang-format 文件。如果找到该文件 .clang-format ,则根据文件中指定的设置应用格式。如果在工作区中找不到 .clang-format 文件,则会根据 C_Cpp.clang_format_fallbackStyle 设置中指定的默认样式应用格式。目前,默认格式样式为“Visual Studio”,它是 Visual Studio 中默认代码格式化程序的近似值。

使用publish_state的时候一定要有非空指针检查!
例如:
if (this->custom_mode_number_ != nullptr) {
this->custom_mode_number_->publish_state(0); // Zero out the custom mode
}不然审核员是不会给你过PR的。
贡献到ESPHome
官方文档说明:Contributing — ESPHome
一定要看文档!!!里面有关于代码开发格式的说明!!!不然后期改死人
完成你的软件开发之后,请在本地编译:
1. 首先,确保已经安装了Docker并且Docker正在运行。
2. 打开PowerShell。你可以通过在开始菜单中搜索"PowerShell"找到它。建议使用"以管理员身份运行"。
3. 使cd命令切换到你的esphome项目所在的目录。例如,如果你的项目c:\edev\esphome,那么你应该输cd c:\edev\esphome。
4. 输入以下命令以获取当前目录的路径,并将其格式化以适应Docker的要求:
$current_dir=(Get-Location).Path.ToLower().Replace(':','').Replace('\','/')5. 最后,运行以下命令以使用esphome-lint检查你的代码:
docker run --rm -v "$($current_dir):/esphome" -it ghcr.io/esphome/esphome-lint script/quicklint请注意,请git commit之后再执行检查操作。
vscode运行esphome程序检查:
- 在Command Prompt终端中执行
cd esphome
python -m venv venv
.\venv\Scripts\activate
pip install -e .- 运行:
python3 script/build_codeowners.py
deactivateAdd Seeed Studio mmWave Kit MR24HPC1 by limengdu · Pull Request #5761 · esphome/esphome (github.com)
提交pr时,必须没有黄色标签且全部无错:

你可以在esphome的根目录的test文件夹中,将你写的组件写到组件的文件夹来进行测试。不要新建test文件!!!

贡献到ESPHome Doc
Windows运行git bash,执行下面的命令就可以在本地部署可视化的文档:
docker run --rm -v "${PWD}/":/workspaces/esphome-docs -p 8000:8000 -it ghcr.io/esphome/esphome-docs路径需要在esphome-doc的根目录,docker运行之后浏览器输入IP地址加端口号8000进入。
举例:
cd ~
python -m venv venv
source venv/Scripts/activate
cd esphome-docs
pip install -r requirements.txt
#装make,使cmd可以执行make命令,如果你已经装过了,就不需要
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
choco install make
make live-html
winpty docker run --rm -v "/c/Users/mengd/Desktop/esphome-docs/":/workspaces/esphome-docs -p 8001:8000 -it ghcr.io/esphome/esphome-docs如果在cmd:
docker run --rm -v "C:/Users/mengd/Desktop/esphome-docs/":/workspaces/esphome-docs -p 8001:8000 -it ghcr.io/esphome/esphome-docs网页地址:
localhost:8001如果报错,还需要在cmd装东西:
npm install -g pagefind
pagefind --version要在本地检查文档更改,首先需要安装 Sphinx(使用 Python 3)。(Cygw运行)
pip install -r requirements.txt --user然后,使用提供的 Makefile 构建更改并启动实时更新 Web 服务器:
# Start web server on port 8000
make live-htmlESPHome缩小图片推荐使用:https://tinypng.com/
文档本身的修复/改进应该转到 esphome-docs 存储库的 current 分支。应针对 next 分支添加新功能。
使用GitHub Action构建一个烧录固件的网站
参考代码:https://github.com/esphome/esphome-project-template
要从使用上面的参考代码作为模板来创建外部组件
在仓库里面需要进行一些设置:


