Expert guide for developing and maintaining ESPHome IoT components with code generation, YAML validation, and multi-platform support
Expert assistant for developing ESPHome components, handling YAML configuration validation, C++ code generation, and multi-platform IoT firmware development.
This skill helps you work with the ESPHome project, which converts YAML configuration files into C++ firmware for microcontrollers (ESP32, ESP8266, RP2040, LibreTiny chips). It guides you through:
ESPHome follows a **code generation architecture**:
1. Python code parses YAML configuration files
2. Generates C++ source code based on configuration
3. Compiles and flashes firmware to microcontroller via PlatformIO
**Key directories**:
**Standard component structure**:
```
components/[component_name]/
├── __init__.py # Configuration schema and code generation
├── [component].h # C++ header file
├── [component].cpp # C++ implementation
└── [platform]/ # Platform-specific implementations
├── __init__.py
├── [platform].h
└── [platform].cpp
```
**Component metadata** (in `__init__.py`):
**Basic schema pattern**:
```python
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
CONF_CUSTOM_PARAM = "custom_param" # Define constants 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_CUSTOM_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_custom_param(config[CONF_CUSTOM_PARAM]))
```
**Common validators**:
**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))
```
**Basic component class**:
```cpp
namespace esphome {
namespace 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_custom_param(int param) { this->param_ = param; }
protected:
std::string key_;
int param_{0};
};
} // namespace my_component
} // namespace esphome
```
**Common component types**:
**Sensor**:
```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)
```
**Add C++ code from Python**:
```python
cg.add(var.set_value(config[CONF_VALUE]))
cg.add_library("libraryname", "1.2.3")
cg.add_build_flag("-DSOME_FLAG")
```
**Platform-specific code**:
```python
if CORE.is_esp32:
cg.add_define("USE_ESP32")
```
**Create component tests** in `tests/components/[component]/`:
```yaml
my_component:
key: "test_value"
custom_param: 100
```
**Run tests**:
```bash
script/test_build_components
script/test_build_components -c my_component
script/test_build_components -t esp32
```
**Validate configuration**:
```bash
esphome config my_config.yaml
esphome compile my_config.yaml
```
**Run linters**:
```bash
python3 script/run-in-env.py pre-commit run
```
Support these platforms when applicable:
Use validators for platform requirements:
```python
cv.only_on(["esp32", "esp8266"])
cv.only_with_arduino
```
**Python**:
**C++**:
1. **Fork & Branch**: Create feature branch in your fork
2. **Make Changes**: Follow all coding conventions
3. **Test**: Create tests for all supported platforms
4. **Lint**: Run `pre-commit` hooks
5. **Commit**: Make descriptive commits
6. **Pull Request**: Submit PR against `dev` branch with prefix like `[component_name] Description`
7. **Documentation**: Update docs in separate `esphome/esphome-docs` repo if needed
**PR title format**: `[component] Description` (e.g., `[display] Fix refresh bug`, `[abc123] Add new sensor`)
**Add Python dependency**:
**Add C++ dependency**:
**Add build flags**:
```python
cg.add_build_flag("-DCUSTOM_FLAG=1")
```
**Common issues**:
**Debug commands**:
```bash
esphome config <file>.yaml # Validate configuration
esphome compile <file>.yaml # Compile without uploading
esphome logs <file>.yaml # View real-time logs
```
**`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
DEPENDENCIES = ["uart"]
CODEOWNERS = ["@username"]
example_sensor_ns = cg.esphome_ns.namespace("example_sensor")
ExampleSensor = example_sensor_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/components/sensor/sensor.h"
#include "esphome/core/component.h"
namespace esphome {
namespace example_sensor {
class ExampleSensor : public sensor::Sensor, public PollingComponent {
public:
void update() override;
void dump_config() override;
};
} // namespace example_sensor
} // namespace esphome
```
**`components/example_sensor/example_sensor.cpp`**:
```cpp
#include "example_sensor.h"
#include "esphome/core/log.h"
namespace esphome {
namespace example_sensor {
static const char *TAG = "example_sensor";
void ExampleSensor::update() {
float value = 25.0; // Read sensor value here
this->publish_state(value);
}
void ExampleSensor::dump_config() {
LOG_SENSOR("", "Example Sensor", this);
}
} // namespace example_sensor
} // namespace esphome
```
Follow these patterns consistently to create high-quality ESPHome components that integrate seamlessly with the existing ecosystem.
Leave a review
No reviews yet. Be the first to review this skill!
# Download SKILL.md from killerskills.ai/api/skills/esphome-component-development-xs5qwk/raw