Skip to content

ESPHome Advanced Guide

DIY ESPHome Advanced

This guide covers advanced ESPHome techniques - from packages and substitutions to multi-sensor projects and external components. Perfect for taking your DIY projects to the next level!


Packages let you reuse configuration across many devices. Instead of copying the same WiFi, API, and OTA configuration to 10 devices, put it in one file and import it.

# device.yaml
substitutions:
device_name: living-room-sensor
friendly_name: "Living Room Sensor"
update_interval: 60s
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
sensor:
- platform: dht
pin: GPIO4
temperature:
name: "${friendly_name} Temperature"
humidity:
name: "${friendly_name} Humidity"
update_interval: ${update_interval}
esphome/
├── common/
│ ├── base.yaml # WiFi, API, OTA
│ ├── sensors.yaml # Common sensors
│ └── wifi.yaml # WiFi config
├── living-room-sensor.yaml
├── bedroom-sensor.yaml
└── secrets.yaml
# Import packages directly from GitHub
substitutions:
device_name: my-sensor
friendly_name: "My Sensor"
packages:
# Official ESPHome packages
voice_assistant:
url: https://github.com/esphome/firmware
files:
- voice-assistant/m5stack-atom-echo.yaml
refresh: 1d
# Community packages
common:
url: https://github.com/olegtarasov/esphome-common
ref: main
files:
- common/base.yaml
refresh: 1d

sensor:
- platform: template
name: "Calculated Value"
lambda: |-
// C++ code here
float temp = id(temperature_sensor).state;
float hum = id(humidity_sensor).state;
// Calculate dew point
float a = 17.27;
float b = 237.7;
float alpha = ((a * temp) / (b + temp)) + log(hum / 100.0);
float dewpoint = (b * alpha) / (a - alpha);
return dewpoint;
update_interval: 60s
unit_of_measurement: "°C"
accuracy_decimals: 1
binary_sensor:
- platform: template
name: "Comfort Zone"
lambda: |-
float temp = id(temperature_sensor).state;
float hum = id(humidity_sensor).state;
// Comfortable: 20-24°C and 40-60% humidity
bool comfortable = (temp >= 20 && temp <= 24) &&
(hum >= 40 && hum <= 60);
return comfortable;

Text Sensors with Jinja2 (ESPHome 2025.7+)

Section titled “Text Sensors with Jinja2 (ESPHome 2025.7+)”
# New in ESPHome 2025.7: Jinja2 in substitutions!
substitutions:
# Dynamic values
compile_time: "{{ now().strftime('%Y-%m-%d %H:%M') }}"
random_suffix: "{{ range(1000, 9999) | random }}"
text_sensor:
- platform: template
name: "Firmware Info"
lambda: |-
return {"Compiled: ${compile_time}"};

# Complete room sensor with all relevant measurements
substitutions:
device_name: room-sensor
friendly_name: "Living Room Multi-Sensor"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
esp32:
board: esp32dev
framework:
type: arduino
# I2C bus for sensors
i2c:
sda: GPIO21
scl: GPIO22
scan: true
# UART for CO2 sensor
uart:
- id: uart_co2
rx_pin: GPIO16
tx_pin: GPIO17
baud_rate: 9600
sensor:
# ===== TEMPERATURE & HUMIDITY =====
- platform: bme280_i2c
address: 0x76
temperature:
name: "${friendly_name} Temperature"
id: temperature
filters:
- sliding_window_moving_average:
window_size: 5
send_every: 1
humidity:
name: "${friendly_name} Humidity"
id: humidity
pressure:
name: "${friendly_name} Pressure"
update_interval: 30s
# ===== CO2 =====
- platform: senseair
uart_id: uart_co2
co2:
name: "${friendly_name} CO2"
id: co2
update_interval: 60s
# ===== LIGHT =====
- platform: bh1750
name: "${friendly_name} Illuminance"
address: 0x23
update_interval: 30s
# ===== PRESENCE (mmWave) =====
# Add LD2410 for presence detection
# ===== CALCULATED: DEW POINT =====
- platform: template
name: "${friendly_name} Dew Point"
lambda: |-
float t = id(temperature).state;
float h = id(humidity).state;
float a = 17.27, b = 237.7;
float alpha = ((a * t) / (b + t)) + log(h / 100.0);
return (b * alpha) / (a - alpha);
unit_of_measurement: "°C"
accuracy_decimals: 1
update_interval: 60s
# ===== CALCULATED: AIR QUALITY INDEX =====
- platform: template
name: "${friendly_name} Air Quality"
lambda: |-
int co2_val = id(co2).state;
if (co2_val < 600) return 100; // Excellent
if (co2_val < 800) return 80; // Good
if (co2_val < 1000) return 60; // Moderate
if (co2_val < 1500) return 40; // Poor
return 20; // Bad
unit_of_measurement: "%"
update_interval: 60s
binary_sensor:
# PIR motion
- platform: gpio
pin: GPIO27
name: "${friendly_name} Motion"
device_class: motion
# Status LED
light:
- platform: esp32_rmt_led_strip
rgb_order: GRB
pin: GPIO25
num_leds: 1
chipset: WS2812
name: "${friendly_name} Status LED"
id: status_led
# Interval for status LED based on CO2
interval:
- interval: 5s
then:
- if:
condition:
lambda: 'return id(co2).state < 800;'
then:
- light.turn_on:
id: status_led
brightness: 30%
red: 0%
green: 100%
blue: 0%
else:
- if:
condition:
lambda: 'return id(co2).state < 1200;'
then:
- light.turn_on:
id: status_led
brightness: 30%
red: 100%
green: 100%
blue: 0%
else:
- light.turn_on:
id: status_led
brightness: 30%
red: 100%
green: 0%
blue: 0%
SensorMeasurementI2C AddressPrice
BME280Temp, Hum, Pressure0x76/0x77~$5
SenseAir S8CO2UART~$40
BH1750Light0x23~$3
LD2410mmWaveUART~$8
AM312PIRGPIO~$2

Total: ~$60 for complete multi-sensor


External components are custom ESPHome components hosted on GitHub. They extend ESPHome with functionality not in the core library.

external_components:
# LD2410 mmWave radar
- source: github://screek-workshop/ld2410@main
components: [ld2410]
refresh: 1d
# Custom sensor from community
- source:
type: git
url: https://github.com/ssieb/esphome_components
components: [serial_csv]
ComponentFunctionGitHub
ld2410mmWave radarscreek-workshop/ld2410
ratgdoGarage doorratgdo/esphome-ratgdo
bluetooth_proxyBLE proxyesphome/bluetooth-proxies
improvEasy WiFi setupesphome/improv
# external_components/my_sensor/__init__.py
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import CONF_ID, UNIT_CELSIUS
my_sensor_ns = cg.esphome_ns.namespace('my_sensor')
MySensor = my_sensor_ns.class_('MySensor', sensor.Sensor, cg.PollingComponent)
CONFIG_SCHEMA = sensor.sensor_schema(
MySensor,
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1
).extend(cv.polling_component_schema('60s'))
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)

# For sharing your projects
substitutions:
name: "my-project"
friendly_name: "My Project"
esphome:
name: "${name}"
friendly_name: "${friendly_name}"
name_add_mac_suffix: true # Unique per device
project:
name: "username.my-project"
version: "1.0.0"
# Dashboard import for easy adoption
dashboard_import:
package_import_url: github://username/esphome-project/project.yaml@v1
import_full_config: false
# Captive portal for first-time setup
wifi:
ap:
ssid: "${name} Setup"
password: "12345678"
captive_portal:

Terminal window
# New command for memory analysis
esphome analyze-memory device.yaml
logger:
level: DEBUG
logs:
# Reduce noise from specific components
component: ERROR
wifi: WARN
# Increase logging for debugging
sensor: DEBUG
api: VERBOSE
# Optimize for battery operation
deep_sleep:
run_duration: 30s
sleep_duration: 5min
# Reduce WiFi power consumption
wifi:
power_save_mode: LIGHT # or HIGH
# Batch sensor updates
sensor:
- platform: adc
filters:
- sliding_window_moving_average:
window_size: 10
send_every: 5

Ofte stillede spørgsmål

What's the difference between packages and !include?
Packages merge intelligently (lists combine, dicts merge), while !include just inserts YAML directly. Use packages when you have multiple files with the same sections (e.g., multiple sensor: blocks).
Can I use secrets in remote packages?
No, remote packages cannot use !secret. Use substitutions with defaults in the package, and override them locally where you can use secrets.
How do I debug lambda expressions?
Use ESP_LOGD() in your lambda: ESP_LOGD("custom", "Value: %f", value); - this shows in the log at DEBUG level.
When should I use external components?
When functionality doesn't exist in ESPHome core, or when the community has made a better implementation. Always check that the component is maintained and compatible with your ESPHome version.


Last updated: December 2025


Kommentarer