Jump to: installing ESPHome, the device YAML, the first flash, over-the-air updates or troubleshooting.
Before you start
ESPHome builds firmware for ESP32, ESP8266 and RP2040 boards from a short YAML file. You describe the hardware (a sensor on these pins, a relay on that one), and ESPHome generates the C++, compiles it and flashes the board. After the first flash over USB, every later update goes over Wi-Fi. It's the quickest way to get from a bare board to a working sensor without writing firmware by hand.
This guide builds a temperature, humidity and pressure sensor with a BME280 on an ESP32 development board, connects it to Home Assistant through ESPHome's native API, and shows the MQTT alternative for other systems. You'll need:
- An ESP32 development board with a USB connector. Most ESP32-WROOM-32 "DevKit" boards work, as do the ESP32-C3 and ESP32-S3 variants with the board name changed.
- A BME280 breakout board. Check it's a BME280, not a BMP280: the BMP280 looks almost identical but has no humidity sensor.
- Four jumper wires and a USB cable that carries data. Many cables sold with chargers only carry power, and they're the most common reason a board "isn't detected".
- A computer with Python 3.10 or later, or Docker.
The wiring is the same as in the ESP32 BME280 Weather Station guide, which builds the same sensor with the Arduino IDE. Comparing the two is a good way to see what ESPHome writes for you.
Wiring
| BME280 pin | ESP32 pin | Notes |
|---|---|---|
| VIN or VCC | 3V3 | Most breakouts accept 3.3 V. Never connect to 5 V unless the board has a regulator. |
| GND | GND | |
| SCL | GPIO22 | The default I²C clock on ESP32 DevKit boards |
| SDA | GPIO21 | The default I²C data line |
The I²C address is 0x76 or 0x77, depending on how the breakout ties its SDO pin. ESPHome scans the bus at boot and logs what it finds, so you don't need to know it in advance.
Installing ESPHome
There are three ways to run ESPHome. They all produce the same firmware.
python3 -m pip install --user pipx
python3 -m pipx ensurepath # then open a new terminal
pipx install esphome
esphome version
pipx installs ESPHome in its own virtual environment, so its dependencies don't clash with other Python tools. pipx upgrade esphome updates it later.
name: esphome
services:
esphome:
image: ghcr.io/esphome/esphome:stable
restart: unless-stopped
network_mode: host
volumes:
- ./config:/config
- /etc/localtime:/etc/localtime:ro
The dashboard is a web interface on port 6052 that edits YAML files, compiles and flashes. network_mode: host lets it find devices on your LAN with mDNS, which doesn't cross Docker's bridge network. The dashboard has no login by default; set USERNAME and PASSWORD environment variables on the service, and don't expose the port beyond your LAN.
If you run Home Assistant OS, the third option is the ESPHome Device Builder add-on, which is the same dashboard.
The device YAML
Create a file called garage-climate.yaml. The device name is used as its hostname, so stick to lowercase letters, digits and hyphens.
esphome:
name: garage-climate
friendly_name: Garage climate
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
api:
encryption:
key: !secret garage_climate_api_key
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "Garage-Climate Fallback"
password: !secret fallback_ap_password
captive_portal:
i2c:
sda: GPIO21
scl: GPIO22
scan: true
sensor:
- platform: bme280_i2c
address: 0x76
update_interval: 60s
temperature:
name: "Temperature"
filters:
- offset: -0.8
humidity:
name: "Humidity"
pressure:
name: "Pressure"
- platform: wifi_signal
name: "Wi-Fi signal"
update_interval: 300s
- platform: uptime
name: "Uptime"
What each block does:
esp32: boardselects the pin map and flash layout.esp32devcovers generic ESP32-WROOM-32 DevKit boards. For other boards, look up the identifier in ESPHome's board list, for exampleesp32-c3-devkitm-1.framework: esp-idfbuilds on Espressif's own framework. The alternative isarduino, which some older components require. Setting it explicitly means the build doesn't change when ESPHome changes its default; the ESP32 platform page lists which framework your version uses when the line is left out.apiturns on ESPHome's native API, which Home Assistant uses. The encryption key keeps other devices on your network from talking to it or reading it.otaallows wireless updates, protected by a password.wifi: apwithcaptive_portalcreates a fallback access point if the board can't join your network, for example after you change your Wi-Fi password. Connect to it with a phone and enter new credentials.i2c: scan: truelogs every I²C address found at boot.filters: offsetcorrects a reading. BME280 breakouts often read a little warm because the board heats itself. Compare against a thermometer you trust and set your own offset, or remove the line.
The secrets file
!secret values come from secrets.yaml in the same directory. It keeps credentials out of device files you might share or commit to a repository:
wifi_ssid: "YOUR_NETWORK_NAME"
wifi_password: "YOUR_WIFI_PASSWORD"
fallback_ap_password: "YOUR_FALLBACK_AP_PASSWORD"
ota_password: "YOUR_OTA_PASSWORD"
garage_climate_api_key: "YOUR_API_KEY"
Replace each value. The fallback access point password needs at least eight characters.
The API key must be 32 random bytes in base64. Generate one with:
openssl rand -base64 32
The dashboard generates a key automatically when you create a device through its wizard. Add secrets.yaml to .gitignore if the directory is under version control.
Validate and compile
esphome config garage-climate.yaml # checks the YAML and prints the full resolved configuration
esphome compile garage-climate.yaml # first build downloads the toolchain: several minutes
The first compile downloads the compiler and framework for the board, which can take ten minutes or more on a Raspberry Pi. Later builds reuse it and take well under a minute. esphome config catches most mistakes, such as wrong indentation, an unknown option or a missing secret, before you spend time compiling.
The first flash, over USB
A new board has no ESPHome firmware, so the first installation has to go over USB. Connect the board and run:
esphome run garage-climate.yaml
ESPHome compiles, then lists the serial ports it found. Choose the USB one, typically /dev/ttyUSB0 or /dev/ttyACM0 on Linux, /dev/cu.usbserial-… on macOS, or COM3 and similar on Windows. When flashing finishes, it starts showing the device's log.
A few things often get in the way of the first flash:
- No serial port appears. Try another cable first. Then check that the USB-to-serial chip's driver is installed: CP210x and CH340 are the common ones, and macOS and Windows may need a driver for the CH340.
Permission deniedon Linux. Add yourself to thedialoutgroup (uucpon Arch) withsudo usermod -aG dialout $USER, then log out and back in.Failed to connect to ESP32: Wrong boot mode detected. Hold the board's BOOT button while the upload starts, and release it when you seeWriting at 0x….
If you run the dashboard in Docker on a server, the board is plugged into your laptop, not the server. Use the dashboard's Install → Manual download option and flash the file from a Chrome or Edge browser with ESPHome Web, which uses the browser's Web Serial support.
Reading the logs
After the flash, the log shows the board booting, joining Wi-Fi and reading the sensor:
[I][i2c.idf:...]: Found i2c device at address 0x76
[C][bme280.sensor:...]: BME280:
[C][bme280.sensor:...]: Address: 0x76
[I][wifi:...]: WiFi Connected!
[D][sensor:...]: 'Temperature': Sending state 21.43000 °C with 1 decimals of accuracy
[D][sensor:...]: 'Humidity': Sending state 48.20117 % with 1 decimals of accuracy
If the scan finds the device at 0x77, change address in the YAML. If it finds nothing at all, check the wiring: SDA and SCL swapped is the usual mistake. You can watch the log over Wi-Fi at any time with esphome logs garage-climate.yaml.
Adding it to Home Assistant
Home Assistant discovers ESPHome devices on the same network through mDNS. A notification appears under Settings → Devices & services. Select Configure and paste the API encryption key from secrets.yaml. The temperature, humidity, pressure, Wi-Fi signal and uptime entities appear on the device page, named from friendly_name and each sensor's name.
If discovery doesn't happen, typically because Home Assistant runs in Docker without host networking, or the two are on different VLANs, add it by hand: Add integration → ESPHome, with the hostname garage-climate.local or the device's IP address and port 6053. The Home Assistant in Docker guide covers the networking that discovery needs.
Using MQTT instead
If you use something other than Home Assistant, or want the readings available to several programs, ESPHome can publish to an MQTT broker such as the one from the Mosquitto guide. Replace the api: block with:
mqtt:
broker: 192.168.1.10
port: 1883
username: garage-climate
password: !secret mqtt_password
discovery: true
Replace the address with your broker's. The device then publishes each sensor to a topic such as garage-climate/sensor/temperature/state, sets a last-will message on garage-climate/status, and, with discovery: true, announces itself to Home Assistant's MQTT integration. Give it its own broker account and an ACL entry that allows writing to garage-climate/# and homeassistant/#.
Over-the-air updates
Once the board runs ESPHome, you can update it from the same computer over Wi-Fi. Change the YAML, for example the update interval or the temperature offset, and run:
esphome run garage-climate.yaml --device garage-climate.local
The board downloads the new firmware into a second partition, checks it, and reboots into it. If the new firmware fails to start, ESP32 boards fall back to the previous one. OTA uploads use port 3232, so a firewall between your computer and the board must allow it.
Check the ESPHome changelog before upgrading ESPHome itself. Breaking changes are listed for each release, and sometimes a YAML option needs renaming before the device will compile again.
Extending the device
Additional components go in the same file. A few common additions:
binary_sensor:
- platform: gpio
pin:
number: GPIO4
mode: INPUT_PULLUP
inverted: true
name: "Garage door"
device_class: garage_door
filters:
- delayed_on_off: 100ms
switch:
- platform: gpio
pin: GPIO26
name: "Fan relay"
restore_mode: RESTORE_DEFAULT_OFF
The binary sensor reads a reed switch wired between GPIO4 and GND. INPUT_PULLUP holds the pin high when the switch is open, inverted makes "closed" read as on, and delayed_on_off filters out contact bounce. The switch drives a relay module; RESTORE_DEFAULT_OFF makes sure it starts off after a power cut. Avoid GPIO0, 2, 5, 12 and 15 for inputs on classic ESP32 boards. Those are strapping pins that affect how the chip boots.
Troubleshooting
The device keeps rebooting with Brownout detector was triggered
The power supply can't deliver enough current during Wi-Fi transmission. Use a better USB port or power supply, and a shorter cable.
Home Assistant shows the device as unavailable
Check that the board is on the network with ping garage-climate.local. If it answers but stays unavailable, the API encryption key in Home Assistant doesn't match the one in secrets.yaml. Delete the integration entry and add it again with the right key.
OTA fails with Connecting to … failed
mDNS isn't resolving across your network, or a firewall blocks port 3232. Pass the IP address instead with --device 192.168.1.42, and look the address up in your router's DHCP list.
Readings are clearly wrong or stuck
A BMP280 instead of a BME280 reports no humidity. A sensor mounted next to the ESP32 reads high from the board's own heat; move it away on longer wires, or add an offset.
Next steps
- Home Assistant in Docker: a home for the readings, with history and automations.
- ESP32 Deep Sleep on Battery: run the same sensor on batteries for months.
- Mosquitto MQTT Broker Setup: the broker for the MQTT option.
- ESPHome's getting started guide and the BME280 component page.