Just a few questions guys

Just made my first ESPHome node using an old ESP32 I had laying around and it’s actually better than I was expecting, however I have a few questions about it’s long term usability.

Firstly, if I understand correctly every node must be unique and have it’s own name? I mean, let’s assume I make 10 devices that do the same thing, so I use the same code, why can’t I have all those devices use a generic device ID and HA modifies it upon discovery like with any other device?
Also, there is an unused entity in all the ESPHome nodes called ‘Firmware’, wouldn’t this be perfect for being to upload a new version and have it propagate to all the nodes at once? As it stands now I would have to select each device one at a time.

Secondly, let’s assume I already have an ESP project and I would like to add ESPHome functionality to this existing project, how is this achieved? I think I read somewhere you can make your own ‘.h’ file in python? Why python? Doesn’t it all get compiled to C anyway? In any case, maybe a few pointers as to how I get existing code into the ESPHome code would helpful.

Just a few thoughts I was having while I was using this thing, I still think it’s pretty cool though, I guess I am looking forward before committing to using this as my go-to for new ESP projects.

Thanks guys.

Normally the renaming is done as part of the setup and generation of the config for the device before or as part of the flashing/install of the device names and ip addressing, may be others unknown would causes issues if conflicts. Suggest that your look at esphome docs in detail.

This will likely be hard unless the existing code is trivial.

You are asking great questions, unfortunately there aren’t great answers to most of them. In my experience, if you want to do trivial things in esphome it is pretty easy. More complex things can work well if you can find an existing example that is close to what you want. The problem is esphome changes fairly quickly (both good and bad) and examples get out of date.

If you want to do custom things, you probably need to create an external component. The docs for this used to be pretty much non-existent. Now there are some, but they really only cover the trivial basics. The good news the code is all open source so you can look at it. Unfortunately, there is little information on why things are done the way they are and if that is really a good thing. For some of the components, they are clearly not working correctly.

I have been using esphome because it makes part of the process easier, but the learning curve is steep. Many people try AI to help, but it typically doesn’t do so well.

ESPHome uses (for better or worse) mDNS, which requires a unique name. You can build a bunch of nodes from a single yaml file using name_add_mac_suffix: but be aware that individual yaml files in builder will be required if you want to do OTA updates:

Unused? It signals when an update is available for that node, for use in automations and for determining if an update is required by HA UI. You can disable the entity if you don’t want the update nag. You can already update multiple nodes at the same time via the Builder UI.

If you already have a complex ESPHome project, that you simply want in HA, add MQTT functionality making sure you generate the appropriate discovery packages. You can also implement the HA API - google for libraries and examples, it’s not something I have used however. For adding your own C++ code - answered below.

I think you are confusing HA add-ons / custom components and ESPHome. ESPHome uses C++. Read the docs - especially this bit, which talks about adding external libraries and your own C++ code:

While you are at it, read all the subheadings under ESPHome configuration, that answers a lot of things:

All good answers. As Neel says, ESPHome has been changing fairly quickly, and the documentation is rather more technical than I like.

Yes, Home Assistant is mainly in Python.
ESPHome is a separate Open Source project built from an Arduino orientation and (I believe) using much of the same libraries, which are in C++. The association comes because many of the ESPHome users were already connecting their devices to Home Assistant.

You might be interested in my smart power plugs as an example of one approach. I have a small yaml file for each device, with most of the actual functionality in common shared files - one for wifi which is common to all my devices, and one specific to that model of smart plug.

At the time (several years back) I was already manually assigning IP addresses, and felt that appending the last octet of IP address to make device names unique made more sense to me than using the MAC address (name_add_mac_suffix).

I was actually surprised at how little ended up being required in the individual device files. Eg, for the Arlec PC191HA smart plug with IP address 192.168.1.106 the yaml file is only:

#
# I have a number of Arlec PC191HA smart plugs with Power Monitoring,  
#
# This contains the parameters specific to this individual device.
### This unit is labelled as "series 2" but is v1 internally ###

# device-specific values moved out of common files
substitutions:
  deviceIP: "106"                       # last octet of the IP Address
  wifi_ssid:      !secret upstairs_ssid     ### which wi-fi to connect to
  wifi_password:  !secret upstairs_password 

                            # This unit is labelled as "series 2" but is v1 internally
  pc191ha_version: "v1"               # versions 1 and 2 use different chips with different pinouts
  update_interval_network: "10 min"   # How often to report network quality values
  update_interval_energy:  "5 min"    # How often to measure and report energy values
  update_interval_sensor:  "5 min"    # How often to measure and report other values
            # shorter time uses more wi-fi & power - but automations react quicker

packages:
  common_wifi: !include _common_wifi.yaml
  device_base: !include _common_pc191ha-v1.yaml   # version 1

The real work is done in the common files.

All my devices use wi-fi, so the same parameters are used by all. I could copy and paste the code - but then something gets out of date, or I need to make the same change in multiple device files … so best to use one common file. Over time I have added logger statements for debugging different issues I had, and even added unrelated fields like ESPHome version number - which i have chosen to leave in in case they are useful in future.

_common_wifi.yaml

###########################################################
#
# Start with the Wi-fi connection
#
#   Sept 2025 Added more sensors, esp for debugginhg which WAP
#             a device has connected to.  prefix all with wifi-
#
#  As at Sept 2025 ESPHome still DOES NOT SUPPORT WI-FI ROAMING,
#    and will sometimes connect to a WAP with lower signal strength.
#
#  When an ESPHome device boots up it scans for the allowable 
#    network (SSID) with highest signal strength. Having made 
#    the connection it does not check for a stronger signal, 
#    even though a stronger signal may later become available. 
#  Even worse, if the scan finds multiple WAPs on the same WLAN
#    it connects to the WAS (BSSID) with lowest channel number 
#    - not the one with strongest signal strength !  
#
###########################################################

wifi:
  ssid:     !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: 192.168.1.${deviceIP}
    gateway: 192.168.1.1
    subnet: 255.255.255.0
  fast_connect: True
#  use_address: 192.168.1.93
  output_power: 10.5      # 8.5-20.5 reduce output power MAY improve Wi-fi in study
  min_auth_mode: WPA2

  ##### add some debugging when compiled on Ponder - will only be seen if USB debugging
  on_connect:
    then:
      lambda: |-
        ESP_LOGI("TEST", "#####     >>>>>>>>>>> WIFI CONNECT");
  on_disconnect:
    then:
      lambda: |-
        ESP_LOGI("TEST", "#####     >>>>>>>>>>> WIFI DISCONNECT");

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "$devicename Fallback"
    password: !secret wifi_ap_password

ota:
  platform: esphome
  password: !secret esphome_ota_password

captive_portal:

# this displays the device's status at http:IP_Address
web_server:
  port: 80

# Enable Home Assistant API
api:
  encryption:
    key: !secret esphome_api_encryption
  ##### add some debugging when compiled on Ponder
  on_client_connected:
    - logger.log:
        format: "#####     >>>>>>>>>>  API Client '%s' connected with IP %s"
        args: ["client_info.c_str()", "client_address.c_str()"]
  on_client_disconnected:
    - logger.log: "#####     >>>>>>>>>> API client disconnected!"

###########################################################
#
#   Add some common sensors - wi-fi signal strength, 
#       uptime, ESPHome version & compile date/time
#
###########################################################

sensor:
  - platform: wifi_signal # Reports the WiFi signal strength/RSSI in dB
    name: WiFi Signal dB
    id: wifi_signal_db
    update_interval: $update_interval_network
    entity_category: "diagnostic"

  - platform: template
    id: wifi_channel
    name: Wifi channel
    entity_category: "diagnostic"

  # human readable uptime sensor output to the text sensor 
  - platform: uptime
    id: uptime_sensor
    name: Uptime
    type: seconds
    update_interval: $update_interval_network


text_sensor:
  - platform: version
    name: ESPHome Version
    hide_timestamp: False

  - platform: template
    id: wifi_node
    name: WLAN node
    update_interval: $update_interval_network
    entity_category: "diagnostic"

  - platform: wifi_info
    bssid:
      id: esp_connected_bssid
      name: Wifi Connected BSSID
      on_value:
        then:
          # lookup friendly name indicating which WAP we connected to
          - lambda: |-
              if ( x == "A0:36:BC:0E:29:38") {
                id(wifi_node).publish_state("LivingRoom");    // ASUS RT-AX55   192/168.1.1
                id(wifi_channel).publish_state(8);
              } else if ( x == "30:5A:3A:C5:B4:20") {
                id(wifi_node).publish_state("Laundry");       // ASUS RT-68U     192.168.1.2
                id(wifi_channel).publish_state(3);
              } else if ( x == "5C:E9:31:1C:C1:69") {
                id(wifi_node).publish_state("Bedroom");       // TP-Link WPA8631 192.168.1.3
                id(wifi_channel).publish_state(11);
              } else if ( x == "64:66:B3:ED:08:C4") {
                id(wifi_node).publish_state("Study");         // TP-Link WR743   192.168.1.4
                id(wifi_channel).publish_state(13);
              } else {
                id(wifi_node).publish_state("Unknown");
                id(wifi_channel).publish_state(-99);
              }


# get the current time from the Home Assistant server
time:
  - platform: homeassistant
    id: homeassistant_time
    update_interval: "12h"         # update from Home Assistant every 12 hours

binary_sensor:
  - platform: status
    name: Wifi "Status"

Unless you have a similar model of smart power plug the following code will be mostly irrelevant for you, but I include it here for completeness, and to show how those substituted values are used (with $ prefix and optional curly braces)

_common_pc191ha-v1.yaml

# I have a number of Arlec PC191HA v1 smart plugs with Power Monitoring,  
#   so this file contains logic common to all, to save updating multiple files.
#
# Version 1 uses WB2S chip, but version 2 uses CB2S chip with different pin numbers 
#   ... so easier to have separate v1 and v2 common files. 
# Except that the units labelled "Version 2" are in fact the same as version 1. 

# ESPHome guide says to use "name_add_mac_suffix: true" to automatically add the  
#     MAC address to the device name, so you can use a single firmware for all devices.  
#     Since I already use static IP addresses, I append last octet of the device IP Address.  
#
#  Many sections include name: and id: which are given the same values. 
#  In fact the id: is used only within the yaml configuration, whereas the external 
#     HA entity is named with friendly_name: and underline and name:
#  So, the code:
#        binary_sensor:        # button   (bt1_pin: 11)
#          - platform: gpio
#            pin: P11
#            name: button
#            device_class: window
#  will generate HA entity named "pc191ha-111 button" with entity ID of "binary_sensor.pc191ha-111-button" 
#

substitutions:
  devicename: pc191ha-${deviceIP} 
  restore_mode: RESTORE_DEFAULT_ON        # mode for when power is turned on

esphome:
  name:          pc191ha-${deviceIP}

bk72xx:
  board: wb2s             # version 1 uses WB2S chip
  framework:
    version: dev

# Enable logging
logger:


###########################################################
#
# PC191HA basic switch operation - button, relay and LED
#
###########################################################
    #
    # button is a binary.sensor.  With the invert filter, the button is "ON" only while the button is being pressed. 
    # switch is the relay which turns the power on/off
    # light is the LED in the switch, so should have same on/off state as the relay
    #   so no point exposing these to HA - use internal: true option
    # there is also a wifi_LED, but it is not seen from outside the case
    # I have added a virtual Reset button, pressed via control panel or long press of the button
    #

binary_sensor:        # button   (bt1_pin: 11)
  - platform: gpio
    pin: P11
    name: Button
    device_class: window
    # by default the button is ON, and momentarily OFF while it is being pressed
    #  invert this, so ON means button is being pressed
    filters:
      - invert:
    # when button is pressed, toggle the switch on/off
    # add long (4second) press to reset
    on_multi_click:
    - timing:
        - ON for at least 4s
        - OFF for at least 1s
      then:
        - logger.log: "Button Long press to reset"
        - button.press: reboot         # long press to reset
    - timing:
        - ON for at most 1s
        - OFF for at least 1s
      then:
        - logger.log: "Button Single Short Clicked"
        - switch.toggle: relay        # toggle the relay / switch
    internal: true                    # button is momentary-on, HA is concerned with relay


switch:              # relay    (rl1_pin: 6)
  - platform: gpio
    pin: P6
    name: Relay
    id:   relay
    restore_mode: $restore_mode       # default when power is turned on
    # synchronise the LED with the relay
    on_turn_on:
      then:
        - light.turn_on: led
    on_turn_off:
      then:
        - light.turn_off: led

light:                # Blue LED in the button  (led1_pin: 26)
  - platform: status_led
    #name:  LED state
    id:   led
    pin: P26
    restore_mode: $restore_mode       # default when power is turned on 
    internal: true          # don't need to see/change state of the LED separate from the relay

button:
  - platform: restart
    name: Reboot
    id: reboot

###########################################################
#
# PC191HA sensors - power monitoring
#
###########################################################
globals:
  - id: total_energy
    type: float
    restore_value: yes
    initial_value: '0.0'

sensor:
    # PC191HA includes a BL0937 chip for measuring power consumption
    #     and BL0937 is a variation of hlw8012, but using inverted SEL pin functionality
    #     Note that the first value reported should be ignored as inaccurate
  - platform: hlw8012
    model: BL0937     # note that the model must be specified to use special calculation parameters
    sel_pin:          # I believe that cf_pin reports either Voltage or Current depending on this select pin
      inverted: true  # determine whether true reports Voltage
      number: P24
    cf_pin:           # current or voltage (ele_pin: 7)
      inverted: true  # the logic of BL0937 is opposite from HLW8012
      number: P7
    cf1_pin:          #  Power (vi_pin: 8)
      inverted: true  # the logic of BL0937 is opposite from HLW8012
      number: P8
    update_interval: ${update_interval_energy}      # How often to measure and report raw values

    ### Decided that I want Power and Voltage reported each time (not swapping with Current).  
    # I choose not to keep swapping SEL pin, instead setting change_mode_every to a
    #   sufficiently high value that it will take 4000 years to change. 
    #   This means I will have to calculate the value for current (as a template) from power / voltage
    initial_mode: "VOLTAGE"           # reports VOLTAGE or CURRENT
    change_mode_every: "never"    #4294967295 # how many times to report before swapping

    # Adjust according to the actual resistor values on board to calibrate the specific unit
    voltage_divider:  770       # LOWER VALUE GIVES LOWER VOLTAGE
    current_resistor: 0.001     # HIGHER VALUE GIVES LOWER WATTAGE

    # how the power monitoring values are returned to ESPHome
    voltage:
      name: Voltage
      id:   voltage
      unit_of_measurement: V
      accuracy_decimals: 2
      filters:
        - skip_initial: 1

    power:
      name: Power
      id:   power_sensor
      unit_of_measurement: W
      accuracy_decimals: 3
      filters:
        - skip_initial: 1
        - multiply: 0.97
        - lambda: if (x < 0.01) {return 0;} else {return x;}

    energy:
      name: Energy
      id: energy
      unit_of_measurement: kWh
      accuracy_decimals: 3
      filters:
        - skip_initial: 1
        - multiply: 0.001  # Multiplication factor from W to kW is 0.001
      on_value:
        then:
          - lambda: |-
              static float previous_energy_value = 0.0;
              float current_energy_value = id(energy).state;
              id(total_energy) += current_energy_value - previous_energy_value;
              previous_energy_value = current_energy_value;


# instead of alternating reporting Voltage and Current, I will report Current from a template 
  - platform: template  
    name: Current
    id: current
    unit_of_measurement: A
    accuracy_decimals: 3
    update_interval: ${update_interval_energy}
    lambda: |-
      return (id(power_sensor).state / id(voltage).state );
    filters:  
      - skip_initial: 2     # give time for data to settle to avoid NaN
    
  - platform: template
    name: Total Energy
    unit_of_measurement: kWh
    update_interval: ${update_interval_energy}
    device_class: "energy"
    state_class: "total_increasing"
    icon: "mdi:lightning-bolt"
    accuracy_decimals: 1
    lambda: |-
      return id(total_energy);

  - platform: total_daily_energy
    name: Total Daily Energy
    power_id: power_sensor
    unit_of_measurement: 'kWh'
    state_class: total_increasing
    device_class: energy
    accuracy_decimals: 3
    filters:
      # Multiplication factor from W to kW is 0.001
      - multiply: 0.001

Have you looked at Tasmota as an alternative to ESPHome to load custom firmware on your ESP32?

Their approach is slightly different, and you may have easier outcomes using one vs the other, especially if you want to avoid rheems of yaml and Python code.