ESPHome Functions?

Old Guy with some long ago programming experience. Now exploring ESPHome and YAML coding. Latest CYD project implements several buttons where what I know as “callable functions” would be very helpful in cleaning up repetitive code. Any similar approach within the ESPHome development environment? And on a related topic, where to learn more about the “Command Palette” that can be invoked within current Device Editor?

Thanks!
Bo3b

Component-Level Substitutions for Dynamic YAML Anchors

Summary

Add support for component-level (local) substitutions that can be used with YAML anchors to enable parameterized, reusable component configurations without code duplication.

Problem Statement

Currently, ESPHome supports substitutions only at the global file level. When using YAML anchors to create reusable component templates, there is no way to pass dynamic parameters (like IDs or entity names) into nested anchor references. This severely limits the usefulness of YAML anchors for DRY (Don’t Repeat Yourself) configurations.

Current Limitation Example

# This does NOT work - substitutions can't be defined at component level
binary_sensor:
  - platform: homeassistant
    <<: *Anchor_Button
    id: button_1
    substitutions:  # ❌ Invalid option
      btn_last_change_id: button_1_last_change

Use Case

A common pattern is creating multiple similar components (buttons, sensors, switches) that differ only in their IDs and related parameters. For example:

Desired Configuration:

  • Multiple binary sensors monitoring Home Assistant entities
  • Each sensor needs to track its last state change timestamp
  • Each sensor writes to its own unique global variable
  • All sensors share identical logic except for ID references

Current Workaround Required: Users must either:

  1. Duplicate the entire on_state block for each component
  2. Use a single shared global variable (losing per-component tracking)
  3. Create complex lambda code with maps/arrays (less maintainable)
  4. Use multiple !include files with global substitution changes (fragile)

Proposed Solution

Option A: Component-Level Substitutions (Preferred)

Allow substitutions: as a valid key within individual component definitions, with scope limited to that component:

substitutions:
  # Global substitutions
  btn_1_device: "Office Desk Lamp"

# Define reusable anchor with substitution placeholder
.globals_set_template: &Anchor_Button_LastChange
  id: ${btn_last_change_id}  # Will be resolved from component-level substitution
  value: !lambda |-
    char time_str[20];
    id(my_time).now().strftime(time_str, sizeof(time_str), "%I:%M:%S %p");
    return std::string(time_str);

.binary_sensor: &Anchor_Button
  internal: true
  on_state:
    then:
      - globals.set:
          <<: *Anchor_Button_LastChange

binary_sensor:
  - platform: homeassistant
    <<: *Anchor_Button
    id: button_1
    entity_id: switch.s31_green_relay
    name: ${btn_1_device}
    substitutions:  # ✅ Component-level substitutions
      btn_last_change_id: button_1_last_change

  - platform: homeassistant
    <<: *Anchor_Button
    id: button_2
    entity_id: switch.s31_blue_relay
    name: ${btn_2_device}
    substitutions:  # ✅ Different value for this component
      btn_last_change_id: button_2_last_change

Behavior:

  • Component-level substitutions override global substitutions within that component’s scope
  • Substitutions are resolved after YAML anchors are merged
  • Scope is limited to the component and its children (nested anchors, lambdas, etc.)

Option B: Lambda Context Variables (Alternative)

Provide automatic context variables in lambdas that reference the parent component:

.binary_sensor: &Anchor_Button
  internal: true
  on_state:
    then:
      - lambda: |-
          // Access parent component ID automatically
          auto component_id = __component_id__;  // New built-in variable
          char time_str[20];
          id(my_time).now().strftime(time_str, sizeof(time_str), "%I:%M:%S %p");
          id(button_last_changes)[component_id] = std::string(time_str);

Built-in context variables could include:

  • __component_id__ - The string ID of the parent component
  • __component_name__ - The friendly name of the parent component
  • __component_type__ - The platform/type (e.g., “homeassistant”)

Benefits

  1. Eliminates Code Duplication: Write complex logic once, reuse everywhere
  2. Improves Maintainability: Changes to shared logic update all instances automatically
  3. Reduces Errors: Single source of truth prevents copy-paste mistakes
  4. Enhances Readability: Configuration intent is clearer with parameterized templates
  5. Backward Compatible: Existing configurations without component-level substitutions continue to work unchanged

Technical Considerations

  • Substitution resolution order: Global → Component-level → Lambda evaluation
  • Circular reference detection needed if component-level substitutions reference other substitutions
  • Documentation should clearly explain scope and resolution order
  • Consider performance impact of multiple substitution passes during config parsing

Alternative Solutions Considered

  1. !include with global substitutions: Fragile, requires changing global state between components
  2. Lambda maps/dictionaries: Works but reduces readability and requires C++ knowledge
  3. Separate anchors per component: Defeats the purpose of DRY principles
  4. ESPHome packages: Too heavyweight for simple parameterization within a single file

Implementation Priority

Medium-High - This would significantly improve configuration maintainability for users with multiple similar components, which is a very common use case in home automation.

Example Real-World Impact

A configuration with 10 similar binary sensors currently requires:

  • ~200 lines of duplicated YAML for the on_state logic
  • Manual synchronization when logic needs to change

With component-level substitutions:

  • ~20 lines for the anchor definition
  • ~50 lines for the 10 sensor declarations (5 lines each)
  • Single point of maintenance for shared logic

Savings: ~130 lines and significantly reduced maintenance burden


Do not promote AI answers as solutions. This is a rule on these forums.

To avoid code duplication, don’t use AI, read docs. Then choose for instance packages. No idea why you think it is heavyweight. It is way more readable and maintainable and could have avoided your duplicate button definitions. Here’s an example of one of my esp’s (left out the boilerplate stuff):

packages:
  diagnostics: !include packages/diags.yaml
  ble_tracker: !include packages/ble_tracker.yaml
  firebeetle: !include packages/firebeetle.yaml
  printer: !include { 
    file: 'packages/temphumsensor.yaml', 
    vars: { 
      device_id: "printer",
      device_name: "3D printer chamber",
      bus_id: bus_a,
      bus_sda: GPIO21,
      bus_scl: GPIO22
    }
  }
  dryer: !include { 
    file: 'packages/temphumsensor.yaml', 
    vars: { 
      device_id: "dryer",
      device_name: "Filament dryer",
      bus_id: bus_b,
      bus_sda: GPIO17,
      bus_scl: GPIO16
    }    
  }
1 Like

Besides the warnings above against using AI, if this is a feature request, it belongs in the ESPHome github. Posting it here won’t accomplish anything.