De-duplicating ESPHome configuration files

It's time to de-duplicate and tidy up some of my ESPHome Configs
This doc is broken down into a few sections, the explanation for why, the shared configurations, and finally my device type configurations, this last one has usage docs.

Update 17th April 2025: restore_switch_state.yaml no longer uses method call/perform, I think this was always broken, but it was compiled and installed fine until ESPHome 2025.4.0

Table of Contents

Why de-duplicate?

I want to make it easier to maintain and apply changes and keep configurations consistent, with my previous blog posts I had things working but they were inconsistent (because I pulled configs from t’internet)

The issues I had were

  • Not all devices restored state, some of my bulbs did, none of my switches did, when power was restored
  • I had inconsistent IDs based on where configuration files came from (rgbww_light vs light_rgbww)
  • I wanted more consistency across the formatting
  • If I decided to add a configuration option in the future, I didn’t want to add it to 20+ devices by hand

Research

I found an initial “simple” solution from the ESPHome documentation, the YAML insertion operator

The issues with this is it literally just adds the content to the current definition, so you can’t have duplicate sections

If your main yaml has

substitutions:
  some_var: value

<<: !include common.yaml

And the common.yaml has

substitutions:
  other_var: other value

You’ll get errors, this wasn’t great for my reduce, reuse, recycle plan.

So then I found out about packages, this is exactly what I wanted since you merge the files this way.

The format here is very similar, but also a bit odd (I wanted to know more about the naming and if I could reference things, but no luck)

packages:
  common: !include common.yaml
  restore_light_state: !include restore_light_state.yaml

Then my files are well configured (below to save repitition) to reduce the duplication

Shared configurations

These aren’t really device specific but are building blocks for the devices

common.yaml

This file defines my ESPHome configs that are common across all my devices

substitutions:
  wifi_fast_connect: "false"

# Common configuration to setup defaults
logger:

web_server:

captive_portal:

mdns:

api:

ota:
  platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
  # Allow rapid re-connection to previously connect WiFi SSID, skipping scan of all SSID
  fast_connect: "${wifi_fast_connect}"

restore_light_state.yaml

This restores the light state based on the individual device configuration, my original configs had light_rgbww or rgbww_light as the light id, which I didn’t like!

# Provides directive to allow the light state to be restored from the previous mode

esphome:
  on_boot:
    then:
      - script.execute: fast_boot_script
      - select.set_index:
          id: power_mode
          index: !lambda |-
            return id(restore_mode)-1;
      - lambda: |-
          switch(id(restore_mode))
          {
          case 1:{
                  auto call = id(rgbww_light).turn_off();
                  call.perform();
                  break;
                  }
          case 2:{
                  auto call = id(rgbww_light).turn_on();
                  call.set_color_mode(ColorMode::WHITE);
                  call.set_brightness(1.0);
                  call.perform();
                  break;
                  }
          default:{
                  break;
                  }
          }

button:
  - platform: factory_reset
    name: "Factory Reset"
    id: Reset
    entity_category: config
globals:
  - id: fast_boot
    type: int
    restore_value: yes
    initial_value: "0"

  - id: restore_mode
    type: int
    restore_value: yes
    initial_value: "1"

select:
  - platform: template
    name: "Power On State"
    id: "power_mode"
    optimistic: true
    options:
      - Always Off
      - Always On
      - Restore Power Off State
    on_value:
      then:
        - lambda: |-
            id(restore_mode)=i+1;
script:
  - id: fast_boot_script
    then:
      - if:
          condition:
            lambda: return ( id(fast_boot) >= 3 );
          then:
            - lambda: |-
                ESP_LOGD("${friendly_name}", "Now the counter is greater than or equal to 3, perform reset device and reboot");
                id(fast_boot) = 0;
                fast_boot->loop();
                global_preferences->sync();
                auto call = id(rgbww_light).turn_on();
                call.set_transition_length(500);
                call.set_brightness(1.0);
                call.set_color_mode(ColorMode::RGB);
                call.set_rgb(0.0, 0.0, 1.0);
                call.perform();
            - delay: 5s
            - button.press: Reset
      - lambda: |-
          id(fast_boot) += 1;
          fast_boot->loop();
          global_preferences->sync();
          ESP_LOGD("${friendly_name}", "Now the counter is %d.  Reset the device when the counter is greater than or equal to 3", id(fast_boot));
      - delay: 10s
      - lambda: |-
          ESP_LOGD("${friendly_name}", "Reset counter");
          id(fast_boot) = 0;
          fast_boot->loop();
          global_preferences->sync();

restore_switch_state.yaml

Similar to restore light state, but for outlets! I could probably deduplicate some more, but this is fine for me for now

Updated 2025/04/17

# Provides directive to allow the switch state to be restored from the previous mode

esphome:
  on_boot:
    then:
      - script.execute: fast_boot_script
      - select.set_index:
          id: power_mode
          index: !lambda |-
            return id(restore_mode)-1;
      - lambda: |-
          switch(id(restore_mode))
          {
          case 1:{
                  id(outlet).turn_off();
                  break;
                  }
          case 2:{
                  id(outlet).turn_on();
                  break;
                  }
          default:{
                  break;
                  }
          }

globals:
  - id: fast_boot
    type: int
    restore_value: yes
    initial_value: "0"

  - id: restore_mode
    type: int
    restore_value: yes
    initial_value: "1"

button:
  - platform: factory_reset
    name: "Factory Reset"
    id: Reset
    entity_category: config

select:
  - platform: template
    name: "Power On State"
    id: "power_mode"
    optimistic: true
    options:
      - Always Off
      - Always On
      - Restore Power Off State
    on_value:
      then:
        - lambda: |-
            id(restore_mode)=i+1;
script:
  - id: fast_boot_script
    then:
      - if:
          condition:
            lambda: return ( id(fast_boot) >= 3 );
          then:
            - lambda: |-
                ESP_LOGD("${friendly_name}", "Now the counter is greater than or equal to 3, perform reset device and reboot");
                id(fast_boot) = 0;
                fast_boot->loop();
                global_preferences->sync();
                id(outlet).turn_on();
            - delay: 5s
            - button.press: Reset
      - lambda: |-
          id(fast_boot) += 1;
          fast_boot->loop();
          global_preferences->sync();
          ESP_LOGD("${friendly_name}", "Now the counter is %d.  Reset the device when the counter is greater than or equal to 3", id(fast_boot));
      - delay: 10s
      - lambda: |-
          ESP_LOGD("${device_name}", "Reset counter");
          id(fast_boot) = 0;
          fast_boot->loop();
          global_preferences->sync();

Devices

These are the device specific files, I add a quick and easy section to copy-and-paste to new devices if I need to re-use or share them with others

globe-50323.yaml

This is my core definition for the Globe 50232 pot lights

## Configuration for Global 50323 Pot Lights
# substitutions:
#   number: "4"

# <<: !include common/globe-50323.yaml

packages:
  common: !include common.yaml
  restore_light_state: !include restore_light_state.yaml

esphome:
  name: "globe-50323-rgbct-recessed-${number}"
  friendly_name: "Globe Lighting 50323 Recessed RGBCT Light (${number})"

bk72xx:
  board: generic-bk7231t-qfn32-tuya

text_sensor:
  - platform: libretiny
    version:
      name: LibreTiny Version

output:
  - platform: libretiny_pwm
    id: output_red
    pin: P8
  - platform: libretiny_pwm
    id: output_green
    pin: P7
  - platform: libretiny_pwm
    id: output_blue
    pin: P6
  - platform: libretiny_pwm
    id: output_cold
    pin: P26
  - platform: libretiny_pwm
    id: output_warm
    pin: P24

light:
  - platform: rgbww
    id: rgbww_light
    name: Light
    color_interlock: true
    cold_white_color_temperature: 6500 K
    warm_white_color_temperature: 2700 K
    red: output_red
    green: output_green
    blue: output_blue
    cold_white: output_cold
    warm_white: output_warm

globe-rgb.yaml

This defines my Globe RGB WW Bulbs

# Copy these to the parent
# substitutions:
#   light_restore_mode: RESTORE_DEFAULT_ON
#   number: "X"
#
# <<: !include common/globe-rgb.yaml

packages:
  restore_light_state: !include restore_light_state.yaml
  common: !include common.yaml

esphome:
  friendly_name: "Globe RGB ${number}"
  name: "globe-rgb-${number}"

bk72xx:
  board: generic-bk7231t-qfn32-tuya

text_sensor:
  - platform: libretiny
    version:
      name: LibreTiny Version

sm2135:
  clock_pin: P8
  data_pin: P26
  rgb_current: 20mA
  cw_current: 55mA

output:
  - platform: sm2135
    id: output_red
    channel: 2
  - platform: sm2135
    id: output_green
    channel: 1
  - platform: sm2135
    id: output_blue
    channel: 0
  - platform: sm2135
    id: output_cold
    channel: 4
  - platform: sm2135
    id: output_warm
    channel: 3

light:
  - platform: rgbww
    id: rgbww_light
    name: Light
    restore_mode: ${light_restore_mode}
    color_interlock: true
    cold_white_color_temperature: 6500 K
    warm_white_color_temperature: 2700 K
    red: output_red
    green: output_green
    blue: output_blue
    cold_white: output_cold
    warm_white: output_warm

button:
  - platform: restart
    name: "Restart"
    entity_category: config

  - platform: safe_mode
    name: "Safe Mode"
    internal: false
    entity_category: config

athom-rgbcw.yaml

This is REALLY complex but I wanted to keep it around to remind myself of the complexity here

# Put these in the parent file
# substitutions:
#   number:
#   # Allows ESP device to be automatically linked to an 'Area' in Home Assistant. Typically used for areas such as 'Lounge Room', 'Kitchen' etc
#   room: ""
#   # Description as appears in ESPHome & top of webserver page
#   device_description: "athom 7w rgbcw light bulb"
#   # Project Name
#   project_name: "Athom Technology.Athom RGBCW Bulb"
#   # Projection version denotes the release version of the yaml file, allowing checking of deployed vs latest version
#   project_version: "v1.1.3"
#   # Restore the light (GPO switch) upon reboot to state:
#   light_restore_mode: RESTORE_DEFAULT_ON
#   # Define a domain for this device to use. i.e. iot.home.lan (so device will appear as athom-smart-plug-v2.iot.home.lan in DNS/DHCP logs)
#   dns_domain: ".local"
#   # Set timezone of the smart plug. Useful if the plug is in a location different to the HA server. Can be entered in unix Country/Area format (i.e. "Australia/Sydney")
#   timezone: ""
#   # Set the duration between the sntp service polling ntp.org servers for an update
#   sntp_update_interval: 6h
#   # Network time servers for your region, enter from lowest to highest priority. To use local servers update as per zones or countries at: https://www.ntppool.org/zone/@
#   sntp_server_1: "0.pool.ntp.org"
#   sntp_server_2: "1.pool.ntp.org"
#   sntp_server_3: "2.pool.ntp.org"
#   # Enables faster network connections, with last connected SSID being connected to and no full scan for SSID being undertaken
#   wifi_fast_connect: "false"
#   # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
#   log_level: "DEBUG"
#   # Enable or disable the use of IPv6 networking on the device
#   ipv6_enable: "false"
#   color_interlock: "true"

packages:
  restore_light_state: !include restore_light_state.yaml
  common: !include common.yaml

esphome:
  name: "athom-rgbcw-bulb-${number}"
  # Default friendly name
  friendly_name: "Athom RGBCW Bulb ${number}"
  comment: "${device_description}"
  area: "${room}"
  name_add_mac_suffix: false
  min_version: 2024.6.0
  project:
    name: "${project_name}"
    version: "${project_version}"

esp8266:
  board: esp8285
  restore_from_flash: true

preferences:
  flash_write_interval: 1min

dashboard_import:
  package_import_url: github://athom-tech/athom-configs/athom-rgbww-light.yaml

binary_sensor:
  - platform: status
    name: "Status"
    entity_category: diagnostic

sensor:
  - platform: uptime
    name: "Uptime Sensor"
    id: uptime_sensor
    entity_category: diagnostic
    internal: true

  - platform: wifi_signal
    name: "WiFi Signal dB"
    id: wifi_signal_db
    update_interval: 60s
    entity_category: "diagnostic"

  - platform: copy
    source_id: wifi_signal_db
    name: "WiFi Signal Percent"
    filters:
      - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
    unit_of_measurement: "Signal %"
    entity_category: "diagnostic"
    device_class: ""

button:
  - platform: restart
    name: "Restart"
    entity_category: config

  - platform: safe_mode
    name: "Safe Mode"
    internal: false
    entity_category: config

output:
  - platform: esp8266_pwm
    id: red_output
    pin: GPIO4
    min_power: 0.000499
    max_power: 1
  - platform: esp8266_pwm
    id: green_output
    pin: GPIO12
    min_power: 0.000499
    max_power: 1
  - platform: esp8266_pwm
    id: blue_output
    pin: GPIO14
    min_power: 0.000499
    max_power: 1
  - platform: esp8266_pwm
    id: warm_white_output
    pin: GPIO13
    min_power: 0.000499
    max_power: 0.9
  - platform: esp8266_pwm
    id: white_output
    pin: GPIO5
    min_power: 0.000499
    max_power: 0.9

light:
  - platform: rgbww
    id: rgbww_light
    name: "RGBCW_Bulb"
    restore_mode: ${light_restore_mode}
    red: red_output
    green: green_output
    blue: blue_output
    warm_white: warm_white_output
    cold_white: white_output
    cold_white_color_temperature: 6000 K
    warm_white_color_temperature: 3000 K
    color_interlock: ${color_interlock}

text_sensor:
  - platform: wifi_info
    ip_address:
      name: "IP Address"
      entity_category: diagnostic
    ssid:
      name: "Connected SSID"
      entity_category: diagnostic
    mac_address:
      name: "Mac Address"
      entity_category: diagnostic

  #  Creates a sensor showing when the device was last restarted
  - platform: template
    name: "Last Restart"
    id: device_last_restart
    icon: mdi:clock
    entity_category: diagnostic
  #    device_class: timestamp

  #  Creates a sensor of the uptime of the device, in formatted days, hours, minutes and seconds
  - platform: template
    name: "Uptime"
    entity_category: diagnostic
    lambda: |-
      int seconds = (id(uptime_sensor).state);
      int days = seconds / (24 * 3600);
      seconds = seconds % (24 * 3600);
      int hours = seconds / 3600;
      seconds = seconds % 3600;
      int minutes = seconds /  60;
      seconds = seconds % 60;
      if ( days > 3650 ) {
        return { "Starting up" };
      } else if ( days ) {
        return { (String(days) +"d " + String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
      } else if ( hours ) {
        return { (String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
      } else if ( minutes ) {
        return { (String(minutes) +"m "+ String(seconds) +"s").c_str() };
      } else {
        return { (String(seconds) +"s").c_str() };
      }
    icon: mdi:clock-start

time:
  - platform: sntp
    id: sntp_time
    # Define the timezone of the device
    timezone: "${timezone}"
    # Change sync interval from default 5min to 6 hours (or as set in substitutions)
    update_interval: ${sntp_update_interval}
    # Set specific sntp servers to use
    servers:
      - "${sntp_server_1}"
      - "${sntp_server_2}"
      - "${sntp_server_3}"
    # Publish the time the device was last restarted
    on_time_sync:
      then:
        # Update last restart time, but only once.
        - if:
            condition:
              lambda: 'return id(device_last_restart).state == "";'
            then:
              - text_sensor.template.publish:
                  id: device_last_restart
                  state: !lambda 'return id(sntp_time).now().strftime("%a %d %b %Y - %I:%M:%S %p");'

globe-smart-plug.yaml

Again, similar to ones above but it uses the restore-switch-state package instead of light

# Globe Electric 50329 smart plug
# Copy this to the new config
# substitutions:
#   number: "1"

# <<: !include common/globe-smart-plug.yaml

packages:
  restore_outlet_state: !include restore_outlet_state.yaml
  common: !include common.yaml

esphome:
  friendly_name: "Globe Plug ${number}"
  name: globe-plug-${number}
  comment: Globe Electric 50329 Smart plug

bk72xx:
  board: wb2s

time:
  - platform: homeassistant
    id: homeassistant_time

sensor:
  - platform: uptime
    name: Uptime
    unit_of_measurement: minutes
    filters:
      - lambda: return x / 60.0;

  - platform: wifi_signal
    name: Signal
    update_interval: 60s

light:
  - platform: status_led
    name: "led"
    internal: true
    id: led
    pin:
      number: P7
      inverted: true

binary_sensor:
  - platform: gpio
    pin:
      number: P26
      inverted: true
    id: button1
    filters:
      - delayed_on: 10ms
      - delayed_off: 10ms
    on_click:
      - switch.toggle: outlet

  - platform: status
    name: Status

switch:
  - platform: gpio
    name: Outlet
    id: outlet
    pin: P24
    icon: mdi:power-socket-us
    on_turn_on:
      - light.turn_on: led
    on_turn_off:
      - light.turn_off: led

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *