
Updated August 2, 2026 for the current ESPHome syntax.
Once you have several ESPHome devices, copying the same Wi-Fi, API, OTA, and diagnostic sensors into every file becomes difficult to maintain. ESPHome substitutions and packages separate shared settings from device-specific hardware: fix a base file once and reuse the change across all your nodes.
This guide covers substitutions, !include, local and remote packages, !extend, !remove, and secrets.yaml. Every copyable example uses the modern esp32: platform block, encrypted API, and the current list-based OTA syntax.
What are ESPHome substitutions?
A substitution is a reusable value. ESPHome processes the top-level substitutions: block before validating YAML and replaces every occurrence of $variable or ${variable}. Names are case-sensitive.
substitutions:
device_name: office-climate
friendly_name: "Office Climate"
update_interval: 60s
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
sensor:
- platform: wifi_signal
name: "Wi-Fi Signal"
update_interval: ${update_interval}Current substitutions can hold numbers, booleans, lists, and dictionaries as well as strings. They also support Jinja expressions inside ${...}, making generic configurations possible without duplicating large YAML blocks.
substitutions:
device:
name: "Uptime"
interval: 30
diagnostics: true
sensor:
- platform: uptime
name: ${device.name}
update_interval: ${device.interval}s
disabled_by_default: ${not device.diagnostics}Command-line substitutions
The -s option overrides values without editing the file. This is useful when validating a template with different node names or boards. With the current CLI, the command comes before the configuration file:
esphome -s device_name office-sensor -s board esp32dev config device-example.yamlCommand-line values take precedence over substitutions declared in YAML. Replace config with run when you are ready to install, but always validate first.
Substitutions vs !include vs packages
| Tool | What it does | Best use |
|---|---|---|
substitutions | Replaces values | Names, pins, intervals, boards, and options |
!include | Inserts another file or fragment | Automations, lists, or specific sections |
packages | Merges complete configurations | Wi-Fi, API, OTA, diagnostics, and common bases |
<<: !include | Uses the traditional YAML merge key | Simple base files; filename substitutions are not supported |
Packages merge non-destructively. Dictionaries merge key by key, components with an id can be modified, and substitutions in the main device file override substitutions from a package.
Recommended file structure
esphome/
├── device-example.yaml
├── secrets.yaml
└── packages/
├── base.yaml
├── wifi.yaml
└── diagnostics.yamlThe device file keeps only its values and hardware-specific components. The downloadable example begins like this:
substitutions:
device_name: office-climate
friendly_name: "Office Climate"
board: esp32dev
log_level: INFO
wifi_ssid: !secret wifi_ssid
wifi_password: !secret wifi_password
fallback_password: !secret office_climate_fallback_password
api_encryption_key: !secret office_climate_api_key
ota_password: !secret office_climate_ota_password
packages:
base: !include packages/base.yaml
wifi: !include packages/wifi.yaml
diagnostics: !include packages/diagnostics.yamlFor an ESP8266, do not put platform: ESP8266 inside esphome:. Replace the platform section in the package with a top-level block:
esp8266:
board: nodemcuv2
restore_from_flash: falseFor practical examples that can share these packages, see the updated ESP32 and BMP280 guide, the DS18B20 guide, and the MH-RD rain sensor project.
Base package with encrypted API and current OTA
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
min_version: 2026.7.0
esp32:
board: ${board}
framework:
type: esp-idf
logger:
level: ${log_level}
api:
encryption:
key: ${api_encryption_key}
ota:
- platform: esphome
password: ${ota_password}This design keeps keys out of the shared package. Each device loads its API key and OTA password from secrets.yaml. Use unique values for every node instead of reusing one key throughout your network.
Customize packages with !extend and !remove
When a component in a package has an id, the main file can modify it without copying the whole definition. The download defines uptime_sensor with a 60-second update interval; this changes it to 30 seconds:
sensor:
- id: !extend uptime_sensor
update_interval: 30sTo remove it completely:
sensor:
- id: !remove uptime_sensorYou can also remove a whole section with captive_portal: !remove. These operations keep shared packages flexible without maintaining several nearly identical copies.
Remote Git packages
ESPHome can download one or more files from GitHub, GitLab, or Codeberg. Pin ref to a release or commit you have reviewed. Following main means an upstream change can enter your next build without warning.
packages:
remote_base:
url: https://github.com/OWNER/REPOSITORY
ref: v1.0.0
files:
- packages/base.yaml
refresh: 1dA remote package cannot read your local
!secretvalues directly. It should expose substitutions with safe defaults, and your local file should provide the secrets. Always review remote code before compiling it.
Share Home Assistant secrets with ESPHome
The simplest option is a dedicated secrets.yaml inside the ESPHome directory. If you want to reuse Home Assistant’s parent file, your ESPHome secrets.yaml can contain:
<<: !include ../secrets.yamlNever publish this file, keep it out of the ZIP, and add it to .gitignore. The download contains only secrets.example.yaml with placeholders you must replace.
Common errors
- Platform not found: place
esp32:oresp8266:at the top level, never insideesphome:. - Substitution not found: check spelling and case;
$device_nameand$Device_Nameare different. - Duplicate ID: two packages define the same identifier. Remove one or use
!extendwhen you intend to modify it. - Could not find file:
!includepaths are relative to the file performing the include. - Remote package error: verify the URL, path,
ref, and repository permissions. - Invalid API key: generate a 32-byte base64 key from ESPHome rather than inventing an arbitrary string.
Always validate before installation:
esphome config device-example.yamlOriginal video and chapters
The original Spanish video remains useful for understanding the workflow, but it was recorded with an older ESPHome release. Use the current YAML in this article and the download instead of copying the platform blocks shown on screen.
- 0:28 — Basic substitutions
- 2:52 — Common file
- 7:42 — Command-line substitutions
- 12:20 — Packages introduction
- 17:15 — Local packages
- 20:00 — Wi-Fi file
- 22:15 — Base package
- 22:40 — API and diagnostics
- 24:48 — Remote packages
- 26:09 — secrets.yaml
Frequently asked questions
Are ESPHome packages obsolete?
No. Local and remote packages remain supported and now cover templates, variables, conditional inclusion, dynamic filenames, !extend, and !remove.
What is the difference between a substitution and a secret?
A substitution avoids repeating a value and may be public. A secret protects sensitive information outside the shared file. You can assign a secret to a substitution and pass it into a package.
Do I need packages for one device?
No. Packages are most valuable when you have several nodes or long reusable blocks that you want to test and maintain independently.
Download and next steps
The ZIP contains the main device file, three local packages, a secrets template, a remote package example, and English and Spanish READMEs. Replace the placeholders, validate the configuration, and then add the hardware specific to your project.
Official references: substitutions, packages, native API, YAML configuration, and security best practices.
If this project helped you, subscribe to Tecnoyfoto on YouTube for more ESPHome and Home Assistant projects.
