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