A cheap Wi-Fi sensor board is a five-dollar way to learn that "smart" usually means "someone else's server". ESPHome takes the opposite approach: you describe the device in YAML, it compiles firmware, and the board speaks plain HTTP and MQTT to your own network. No account, no app, no outage when a vendor pivots.
This is a complete first device: ESP32 + BME280, temperature, humidity and pressure, with OTA updates so you never plug it in again.
Hardware
- ESP32 dev board (any ESP32-WROOM variant)
- BME280 breakout (I²C, 3.3 V — the BME680 works too, the BME280 is cheaper and more accurate for humidity)
- Four jumper wires
Wire it to the default I²C pins: 3V3 → VIN, GND → GND, GPIO22 → SCL, GPIO21 → SDA.
1. ESPHome in Docker
services:
esphome:
image: ghcr.io/esphome/esphome:2026.9
restart: unless-stopped
network_mode: host # mDNS discovery + OTA need host networking
volumes:
- /srv/esphome:/config
environment:
TZ: Europe/Athens
network_mode: host is not laziness: ESPHome discovers boards over mDNS and flashes them over the network, both of which break under bridge networking.
2. The device configuration
# /srv/esphome/office-sensor.yaml
esphome:
name: office-sensor
friendly_name: Office Sensor
min_version: 2026.9.0
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
level: INFO
api:
encryption:
key: !secret api_key
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ap_timeout: 5min # fallback hotspot if the network is down
i2c:
sda: GPIO21
scl: GPIO22
scan: true
sensor:
- platform: bme280_i2c
address: 0x76
update_interval: 60s
temperature:
name: "Office Temperature"
filters:
- sliding_window_moving_average:
window_size: 5
send_every: 5
humidity:
name: "Office Humidity"
accuracy_decimals: 1
pressure:
name: "Office Pressure"
filters:
- multiply: 0.01 # hPa, not Pa
web_server:
port: 80
Three of those blocks earn their place:
api.encryption— the native API is not authenticated by default. Without a key, anyone on the LAN can drive the device.ota+ap— OTA means a firmware change is aesphome runaway; the fallback AP means a Wi-Fi password change does not require a USB cable.filters— raw BME280 readings jitter by ±0.3 °C. A sliding window average makes the dashboard readable and halves the state writes.
3. Secrets, not YAML
# /srv/esphome/secrets.yaml
wifi_ssid: "lab-iot"
wifi_password: "<password>"
ota_password: "<password>"
api_key: "<32-byte base64 key>"
Generate the API key with openssl rand -base64 32. Keep secrets.yaml at mode 600; ESPHome loads it at compile time and embeds the values.
4. Compile, flash once over USB, then OTA
docker compose exec esphome esphome config /config/office-sensor.yaml # validate
docker compose exec esphome esphome run /config/office-sensor.yaml # first flash
# later, after editing:
docker compose exec esphome esphome run /config/office-sensor.yaml --device office-sensor.local
esphome config first is worth the extra ten seconds: it catches YAML indentation and secret typos before you wait for a two-minute compile.
5. Get the data out
Three options, in increasing order of usefulness:
# 1. plain HTTP, handy for a quick check
curl -s http://office-sensor.local/sensor/office_temperature | jq
# 2. MQTT for Home Assistant or anything else
# add to the YAML:
# mqtt:
# broker: 192.168.10.20
# topic_prefix: esphome/office
# 3. the native API, for Home Assistant's ESPHome integration
MQTT is the portable choice — a mosquitto_sub -t 'esphome/+/sensor/+/state' proves the whole pipeline in one line.
Pitfalls
- BME280 at 0x76 vs 0x77. Two devices on one bus must differ;
i2c: scan: trueprints what is actually there at boot. - Power. An ESP32 with Wi-Fi peaks around 500 mA during transmit. A weak USB charger causes brownout resets that look exactly like a firmware bug.
- Blocking
delay()in lambdas. It starves the API and drops the connection. Useinterval:blocks instead. - Flashing over OTA after a network change. If the board cannot reach the new network it will not come back over OTA — that is what the fallback AP is for.
- Secrets in the YAML.
secrets.yamlis committed more often than anyone admits. Add it to.gitignorebefore the first commit.
Total cost for the sensor: under fifteen euros, and the data never leaves your house.