Documentation Index
Fetch the complete documentation index at: https://mintlify.com/esphome/esphome.io/llms.txt
Use this file to discover all available pages before exploring further.
When you manage more than a handful of ESPHome devices, copy-pasting the same Wi-Fi credentials, API keys, or sensor definitions into every YAML file quickly becomes a maintenance burden. ESPHome solves this with two complementary tools: substitutions (named variables you embed in YAML values) and packages (reusable YAML files you include and merge). Together they let you build a single source of truth for shared config while keeping per-device differences minimal and explicit.
Substitutions
A substitution is a named variable whose value is injected wherever ${variable_name} appears in your YAML before ESPHome validates or compiles it.
Declaring Substitutions
substitutions:
device_name: living-room-sensor
temp_offset: "-1.5"
update_interval: 30s
esphome:
name: ${device_name}
sensor:
- platform: bme280_i2c
temperature:
name: "${device_name} Temperature"
filters:
- offset: ${temp_offset}
update_interval: ${update_interval}
Both $variable_name and ${variable_name} are valid; the braced form is recommended because it cannot be ambiguous next to adjacent text.
Substitution Types
Substitution values can be any valid YAML type — strings, numbers, booleans, or even complex objects:
substitutions:
device:
name: Kitchen AC
port: 12
enabled: true
api_key: "abc123"
sensor_pin:
number: 4
inverted: true
Access nested values with dot notation or index syntax inside Jinja expressions:
esphome:
name: ${ device.name | lower | replace(" ", "-") }
binary_sensor:
- platform: gpio
name: "Sensor on pin ${ sensor_pin.number }"
pin: ${sensor_pin}
Jinja Expressions
ESPHome supports simple Jinja expressions inside ${ ... }, including arithmetic, conditionals, and filters:
substitutions:
native_width: 480
native_height: 320
high_dpi: true
scale: 1.5
display:
- platform: ili9xxx
dimensions:
width: ${native_width * 2 if high_dpi else native_width}
height: ${native_height * 2 if high_dpi else native_height}
Python’s math library is also available: ${math.sqrt(x*x + y*y)}.
Command-Line Overrides
Override any substitution at compile time with -s KEY VALUE. You can supply -s multiple times:
# Compile with a different device name
esphome -s device_name porch-sensor run sensor-base.yaml
# Multiple overrides
esphome -s device_name garage -s temp_offset "-2.0" run sensor-base.yaml
Command-line substitutions take precedence over those declared in the YAML file. This makes it easy to maintain a single “template” YAML and stamp out unique devices from CI or a script:
# sensor-base.yaml — used for all temperature sensors
substitutions:
device_name: placeholder # always overridden from CLI
temp_offset: "0.0"
esphome:
name: ${device_name}
sensor:
- platform: bme280_i2c
address: 0x76
temperature:
name: "${device_name} Temperature"
filters:
- offset: ${temp_offset}
esphome -s device_name kitchen -s temp_offset "-1.0" run sensor-base.yaml
esphome -s device_name bedroom run sensor-base.yaml
Packages
Packages let you split your ESPHome configuration across multiple YAML files and merge them together at build time. All definitions from packages are merged non-destructively: dictionaries merge key-by-key, component lists merge by ID, and plain values from the main file take priority over those in packages.
Local Packages
Break shared config into separate files and include them under packages::
# config/wifi.yaml
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
fast_connect: true
# config/base.yaml
esphome:
name: ${node_name}
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
level: INFO
api:
encryption:
key: !secret api_key
ota:
- platform: esphome
password: !secret ota_password
# living-room.yaml — device-specific file
substitutions:
node_name: living-room
packages:
- !include config/wifi.yaml
- !include config/base.yaml
sensor:
- platform: bme280_i2c
temperature:
name: "Living Room Temperature"
humidity:
name: "Living Room Humidity"
Hide shared base files from the ESPHome dashboard by placing them in a subdirectory (the dashboard only lists files in the top-level directory) or by prefixing their names with a dot, e.g. .base.yaml.
Packages as Templates with Variables
Because packages are loaded via !include, you can pass variables to them — turning a package into a parameterised template. Multiple garage doors sharing one template:
# garage-door.yaml — template package
switch:
- platform: gpio
name: "${door_name} Garage Door Switch"
id: ${door_name}_switch
pin: ${door_pin}
cover:
- platform: time_based
name: "${door_name} Garage Door"
open_action:
- switch.turn_on: ${door_name}_switch
open_duration: 30s
close_action:
- switch.turn_on: ${door_name}_switch
close_duration: 32s
stop_action:
- switch.turn_off: ${door_name}_switch
# home.yaml
packages:
left_door: !include
file: garage-door.yaml
vars:
door_name: left
door_pin: GPIO16
right_door: !include
file: garage-door.yaml
vars:
door_name: right
door_pin: GPIO17
Remote (Git) Packages
Packages can be fetched directly from a Git repository. This is ideal for distributing shared configurations across a team or publishing reusable ESPHome libraries:
packages:
# Shorthand: github://username/repository/path/file.yaml[@ref]
shared_base: github://my-org/esphome-common/base.yaml@main
# Full form with multiple files and a refresh interval
sensors:
url: https://github.com/my-org/esphome-common
files:
- sensors/bme280.yaml
- sensors/motion.yaml
ref: v2.1.0
refresh: 1d # re-fetch from GitHub at most once per day
Remote packages cannot use !secret lookups — secrets are only available in the local config directory. Use substitutions with defaults in your remote package, and provide the actual values via the local YAML’s substitutions: block.
Dynamic File Selection with Substitutions
The filename in a !include can itself contain substitution expressions, letting you switch between hardware-specific files based on a variable:
# config.yaml
substitutions:
platform: esp32 # change to esp8266 for older boards
packages:
hardware: !include device-${platform}.yaml
# device-esp32.yaml
substitutions:
LED_GPIO: GPIO2
esp32:
board: esp32dev
# device-esp8266.yaml
substitutions:
LED_GPIO: GPIO2
esp8266:
board: nodemcuv2
Extending and Removing Package Entries
Use !extend <id> to override specific fields of a component defined in a package, without duplicating the entire block:
# common.yaml
sensor:
- platform: uptime
id: uptime_sensor
update_interval: 5min
# fast-device.yaml — need more frequent updates
packages:
- !include common.yaml
sensor:
- id: !extend uptime_sensor
update_interval: 10s # overrides the package value
Remove a package component entirely with !remove:
packages:
- !include common.yaml
# This device doesn't need the uptime sensor
sensor:
- id: !remove uptime_sensor
YAML Merge Key (<<: !include)
For a simpler single-file inheritance pattern, use the YAML merge operator:
# .base.yaml
esphome:
name: ${devicename}
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
logger:
api:
ota:
- platform: esphome
# kitchen.yaml
substitutions:
devicename: kitchen
<<: !include .base.yaml
sensor:
- platform: bme280_i2c
temperature:
name: "Kitchen Temperature"
The <<: !include merge operator does not support substitution expressions in the filename. Use the packages: key with !include if you need dynamic filenames.
Pattern: Board-Specific Substitutions
A common pattern for devices with different ESP variants is to keep all the hardware-specific details in a substitutions block and the shared application logic in a package:
# esp32-node.yaml
substitutions:
node_name: my-esp32-node
board: esp32dev
platform: esp32
status_led_pin: GPIO2
packages:
app: !include shared-app.yaml
esp32:
board: ${board}
framework:
type: esp-idf
# shared-app.yaml
api:
ota:
- platform: esphome
light:
- platform: status_led
name: "Status LED"
pin: ${status_led_pin}
sensor:
- platform: wifi_signal
name: "${node_name} Wi-Fi Signal"
update_interval: 60s
See Also