Expert guidance for developing ESPHome components - IoT firmware system for ESP32/ESP8266/RP2040 using YAML configs and C++ code generation. Includes architectural patterns, coding standards, and testing workflows.
Expert guidance for developing components in ESPHome, a system to configure microcontrollers (ESP32, ESP8266, RP2040, LibreTiny chips) using YAML configuration files that generate C++ firmware for home automation.
ESPHome follows a **code-generation architecture**: Python code parses YAML configs and generates C++ source code, which is compiled via PlatformIO and flashed to microcontrollers.
1. **ESP32** (`components/esp32/`) - Multiple variants (Original, C2, C3, C5, C6, H2, P4, S2, S3), ESP-IDF framework
2. **ESP8266** (`components/esp8266/`) - Arduino only, memory-constrained
3. **RP2040** (`components/rp2040/`) - Raspberry Pi Pico, Arduino with PIO support
4. **LibreTiny** (`components/libretiny/`) - Realtek and Beken chips
Create components following this standard structure:
```
components/[component_name]/
├── __init__.py # Configuration schema and code generation
├── [component].h # C++ header
├── [component].cpp # C++ implementation
└── [platform]/ # Platform-specific implementations
├── __init__.py
├── [platform].h
└── [platform].cpp
```
Define in `__init__.py`:
```python
DEPENDENCIES = ["required_component"]
AUTO_LOAD = ["auto_loaded_component"]
CONFLICTS_WITH = ["incompatible_component"]
CODEOWNERS = ["@github_username"]
MULTI_CONF = True # Allow multiple instances
```
```python
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_KEY, CONF_ID
CONF_PARAM = "param" # New constant not in esphome/const.py
my_component_ns = cg.esphome_ns.namespace("my_component")
MyComponent = my_component_ns.class_("MyComponent", cg.Component)
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(MyComponent),
cv.Required(CONF_KEY): cv.string,
cv.Optional(CONF_PARAM, default=42): cv.int_,
}).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add(var.set_key(config[CONF_KEY]))
cg.add(var.set_param(config[CONF_PARAM]))
```
```cpp
namespace esphome::my_component {
class MyComponent : public Component {
public:
void setup() override;
void loop() override;
void dump_config() override;
void set_key(const std::string &key) { this->key_ = key; }
void set_param(int param) { this->param_ = param; }
protected:
std::string key_;
int param_{0};
};
} // namespace esphome::my_component
```
**Sensor Component:**
```python
from esphome.components import sensor
CONFIG_SCHEMA = sensor.sensor_schema(MySensor).extend(
cv.polling_component_schema("60s")
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
```
**Binary Sensor:**
```python
from esphome.components import binary_sensor
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend({...})
async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
```
**Switch:**
```python
from esphome.components import switch
CONFIG_SCHEMA = switch.switch_schema().extend({...})
async def to_code(config):
var = await switch.new_switch(config)
```
**Naming Conventions:**
**Field Visibility Rules:**
1. Pointer lifetime issues (setters validate pointers from known lists)
2. Invariant coupling (fields must stay synchronized)
3. Resource management (setters perform cleanup/registration)
**Preprocessor Directives:**
- Conditional compilation (`#ifdef`, `#ifndef`)
- Compile-time sizes from Python codegen (e.g., `cg.add_define()`)
**Additional Conventions:**
**Common Validators:**
```python
cv.int_, cv.float_, cv.string, cv.boolean
cv.int_range(min=0, max=100), cv.positive_int, cv.percentage
cv.All(cv.string, cv.Length(min=1, max=50))
cv.Any(cv.int_, cv.string)
```
**Platform Filtering:**
```python
cv.only_on(["esp32", "esp8266"])
cv.only_on_esp32, cv.only_on_esp8266, cv.only_on_rp2040
esp32.only_on_variant(...)
cv.only_with_arduino, cv.only_with_esp_idf
```
**Schema Extensions:**
```python
CONFIG_SCHEMA = cv.Schema({...})
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(i2c.i2c_device_schema(0x48))
.extend(spi.spi_device_schema(cs_pin_required=True))
```
1. Use Docker container OR create Python virtual environment
2. Install dependencies: `pip install -r requirements_dev.txt`
3. Run commands via: `python3 script/run-in-env.py <command>`
**Python Tests:**
```bash
pytest
```
**C++ Static Analysis:**
```bash
clang-tidy
```
**Component Tests (YAML-based):**
```bash
./script/test_build_components -c <component>
./script/test_build_components -t <platform>
./script/test_component_grouping.py -e config --all
```
**Linting:**
```bash
python3 script/run-in-env.py pre-commit run
```
When developing ESPHome components:
1. **Read existing components** in `/esphome/components` for reference patterns
2. **Define clear schemas** with proper validation and platform constraints
3. **Use `protected` fields** by default; only use `private` for safety-critical cases
4. **Avoid preprocessor constants** - use `const` variables or enums
5. **Prefix member access** with `this->` in C++ code
6. **Test on target platforms** using component test scripts
7. **Add defines to `defines.h`** when using `cg.add_define()` for static analysis
8. **Follow naming conventions** strictly (snake_case functions, CamelCase classes)
9. **Document metadata** (DEPENDENCIES, AUTO_LOAD, CONFLICTS_WITH, CODEOWNERS)
10. **Run pre-commit hooks** before submitting changes
**`components/example_sensor/__init__.py`:**
```python
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import CONF_ID, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS
CONF_UPDATE_INTERVAL = "update_interval"
example_ns = cg.esphome_ns.namespace("example_sensor")
ExampleSensor = example_ns.class_("ExampleSensor", sensor.Sensor, cg.PollingComponent)
CONFIG_SCHEMA = sensor.sensor_schema(
ExampleSensor,
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
state_class=STATE_CLASS_MEASUREMENT,
).extend(cv.polling_component_schema("60s"))
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
```
**`components/example_sensor/example_sensor.h`:**
```cpp
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome::example_sensor {
class ExampleSensor : public sensor::Sensor, public PollingComponent {
public:
void update() override;
void dump_config() override;
protected:
float last_value_{0.0f};
};
} // namespace esphome::example_sensor
```
**`components/example_sensor/example_sensor.cpp`:**
```cpp
#include "example_sensor.h"
#include "esphome/core/log.h"
namespace esphome::example_sensor {
static const char *const TAG = "example_sensor";
void ExampleSensor::update() {
float value = 25.0f; // Example: read from hardware
this->publish_state(value);
this->last_value_ = value;
}
void ExampleSensor::dump_config() {
LOG_SENSOR("", "Example Sensor", this);
}
} // namespace esphome::example_sensor
```
Leave a review
No reviews yet. Be the first to review this skill!
# Download SKILL.md from killerskills.ai/api/skills/esphome-component-development-pbhmwg/raw