Skip to content

ESPHome Substitutions and Packages: Practical YAML Guide

02/08/2026
Reusable YAML files and packages connected to an ESP32 board with ESPHome

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.yaml

Command-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

ToolWhat it doesBest use
substitutionsReplaces valuesNames, pins, intervals, boards, and options
!includeInserts another file or fragmentAutomations, lists, or specific sections
packagesMerges complete configurationsWi-Fi, API, OTA, diagnostics, and common bases
<<: !includeUses the traditional YAML merge keySimple 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.yaml

The 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.yaml

For 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: false

For 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: 30s

To remove it completely:

sensor:
  - id: !remove uptime_sensor

You 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: 1d

A remote package cannot read your local !secret values 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.yaml

Never 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: or esp8266: at the top level, never inside esphome:.
  • Substitution not found: check spelling and case; $device_name and $Device_Name are different.
  • Duplicate ID: two packages define the same identifier. Remove one or use !extend when you intend to modify it.
  • Could not find file: !include paths 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.yaml

Original 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.

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.