The Cloud-Free Smart Home Starts With You, Not Amazon
Every “smart” gadget you buy at Costco phones home. The light bulb talks to the cloud. The motion sensor tweets to California about your bathroom habits. The thermostat? Already filed a report about when you’re home. It’s not paranoia — it’s in the terms of service you didn’t read.
ESPHome burns that script. Instead of buying devices that spy for you, you build them. A $12 ESP32 board, some sensors, YAML config instead of Arduino sketches, and boom — you’ve got a custom smart sensor or switch that talks only to your own server. No cloud. No subscriptions. No surprise privacy policy changes at 2 AM.
This is the tutorial I wish I’d had two years ago. Let’s skip the Arduino C++ nonsense and build something that actually works.
What Even Is ESPHome?
ESPHome is declarative firmware for microcontrollers. Write YAML, press a button, and it compiles and flashes your device. That’s it.
The boring version: You define what sensors, switches, and lights your board has in a simple YAML file. ESPHome turns that into a C++ sketch, compiles it, and flashes it to your ESP32/ESP8266/RP2040 board over USB (once) or WiFi (forever after). The device connects to your Home Assistant instance, self-discovers, and shows up as automatable entities.
The real version: You get all the tedious IoT boilerplate — WiFi management, OTA updates, sensor reading loops, debouncing — for free. You’re not wrestling with Arduino libraries or debugging floating-point math at 3 AM. You describe what you want, and ESPHome handles the rest.
No cloud. No registration. No API keys (except the one your Home Assistant instance uses to talk to your device locally).
Why This Beats Writing Arduino Sketches
Let me be honest: Arduino C++ works great. If you like fighting with header files and library version mismatches at midnight, great. I do not.
ESPHome gives you:
Declarative syntax. You’re not writing functions; you’re describing hardware. - platform: dht and it reads temperature and humidity. - platform: gpio and it toggles a relay. No function bodies. No state management. No “wait, did I call setup() first?”
OTA updates baked in. Flash your device once over USB. After that, you push updates over WiFi from your computer. No reaching for the USB cable, no power cycling nonsense. Change a sensor threshold? Hit “upload” in the dashboard and it’s done in 30 seconds.
Home Assistant integration out of the box. Your ESPHome device speaks the ESPHome API natively. Home Assistant discovers it via Zeroconf (mDNS), auto-populates the Integrations list, and all your sensors/switches/fans show up without configuration.yaml cruft.
Secrets management. Instead of hardcoding your WiFi password in YAML, you use secrets.yaml. Check YAML into git, keep secrets local. Simple.
Packages and includes. Write a config once, reuse it. !include lets you split YAML across files or use shared templates. Tired of copy-pasting the same WiFi and API blocks? Package it once.
That’s not nothing. That’s the difference between 20 minutes of “let me flash a moisture sensor” and 2 hours of “why isn’t my library compiling?”
Getting Started: The 30-Minute Path
You need three things:
-
A board: ESP32 (full featured, cheap, ~$12), ESP8266 (older, lower power, WiFi only), or RP2040 (Raspberry Pi’s microcontroller, also cheap). Any will do for learning. I’m using a Wemos D1 Mini (ESP8266) for battery-powered sensors and a Lolin32 (ESP32) for always-on stuff.
-
ESPHome: Either the web-based dashboard (runs on your Home Assistant instance or standalone) or the CLI tool on your laptop.
-
Home Assistant: If you don’t have it yet, read my Nextcloud + Home Assistant setup guide. It’s 20 minutes to a bare metal instance or a Docker container.
Option A: ESPHome Dashboard (easier)
If you run Home Assistant, open Settings → Devices & Services → ESPHome, and you’ll see an “Create New Device” button. It walks you through naming your board and picking the ESP chip type, then generates the initial YAML. From there, you plug in your USB board, hit “Install,” and it flashes via the browser (needs Chromium). After that, you edit YAML in the web editor and hit “Install” again for OTA updates.
Option B: ESPHome CLI (scriptable)
pip install esphomeesphome dashboard ~/esphomeThis spins up a local web dashboard on http://localhost:6052. Same workflow as above, but you manage the YAML files directly on disk.
I use the CLI because I like git-tracking my device configs and editing in my editor. You do you.
The YAML Structure: Everything You Need
Here’s the anatomy of an ESPHome device:
esphome: name: "bedroom-sensor" friendly_name: "Bedroom Temp + Humidity"
esp32: board: lolin_s3
wifi: ssid: !secret wifi_ssid password: !secret wifi_password ap: ssid: "Bedroom-Sensor Fallback" password: "captive123"
api: encryption: key: !secret api_encryption_key
ota: - platform: esphome password: !secret ota_password
web_server: port: 80
sensor: - platform: dht pin: GPIO4 model: DHT22 temperature: name: "Bedroom Temperature" unit_of_measurement: "°C" accuracy_decimals: 1 humidity: name: "Bedroom Humidity" accuracy_decimals: 1 update_interval: 60s
switch: - platform: gpio name: "Bedroom Fan" pin: GPIO5 restore_mode: RESTORE_DEFAULT_OFFLine by line:
esphome:— Defines the device name (becomes its hostname on WiFi) and friendly name (what shows up in Home Assistant).esp32:— Specifies your board type.lolin_s3,esp32_s3_devkitc_1, etc. Look up your exact board.wifi:— Credentials fromsecrets.yaml. Theap:block sets up a fallback hotspot if WiFi isn’t available (useful for reconfiguring).api:— Encryption key so only your Home Assistant can talk to your device. ESPHome generates one on first run.ota:— Over-the-air updates. Once the device boots, you can reflash it over WiFi.web_server:— Runs a simple HTTP server on the device itself (optional, but nice for debugging).sensor:— The good stuff. Define temperature, humidity, motion, light, whatever. Eachplatformis a hardware type (DHT, analog, GPIO input, I2C, etc.).switch:— GPIO outputs you can toggle from Home Assistant. Relay, LED, motor control, whatever.
Project 1: Bedroom Temp/Humidity Sensor (The Warm-Up)
You need:
- ESP32 or ESP8266 ($10–15)
- DHT22 sensor ($5)
- USB cable to flash
Wire it:
- DHT22 VCC → 3.3V
- DHT22 GND → GND
- DHT22 DATA → GPIO4 (configurable)
- Pull-up resistor: 10kΩ between DATA and VCC (comes pre-soldered on most breakout boards)
Config:
esphome: name: "bedroom-sensor"
esp32: board: lolin32
wifi: ssid: !secret wifi_ssid password: !secret wifi_password
api: encryption: key: !secret api_encryption_key
ota: - platform: esphome
sensor: - platform: dht pin: GPIO4 model: DHT22 temperature: name: "Bedroom Temperature" filters: - offset: -0.5 # calibration if your sensor reads hot humidity: name: "Bedroom Humidity" update_interval: 60sFlash it, power it on, and it’ll show up in Home Assistant within 30 seconds. Create an automation: if humidity > 60%, turn on the fan. Done.
Project 2: Motion + Light Sensor (Night Mode Lights)
You need:
- ESP32
- PIR motion sensor (HC-SR501, $3)
- LDR (light-dependent resistor, $1) with a 10kΩ pulldown
- Relay or MOSFET to control a light
Wire it:
- PIR out → GPIO14 (digital input)
- LDR → ADC0 (analog input) with 10kΩ pulldown to GND
- Relay control → GPIO5
Config:
esphome: name: "hallway-light-switch"
esp32: board: lolin32
wifi: ssid: !secret wifi_ssid password: !secret wifi_password
api: encryption: key: !secret api_encryption_key
ota: - platform: esphome
binary_sensor: - platform: gpio name: "Hallway Motion" pin: GPIO14 device_class: motion filters: - delayed_on: 200ms # debounce - delayed_off: 30s # hold motion "true" for 30s after last trigger
sensor: - platform: adc pin: A0 name: "Hallway Light Level" unit_of_measurement: lux device_class: illuminance filters: - exponential_moving_average: alpha: 0.1 send_every: 10
switch: - platform: gpio name: "Hallway Lights" pin: GPIO5Now in Home Assistant, create an automation:
- trigger: platform: state entity_id: binary_sensor.hallway_motion to: "on" condition: condition: numeric_state entity_id: sensor.hallway_light_level below: 200 # only at night action: service: switch.turn_on entity_id: switch.hallway_lightsThe light turns on only when motion is detected and it’s dark. Turn it off manually or wait 2 minutes. No fancy hub required.
Project 3: Garage Door Opener (The Relay Project)
This one uses a relay to trigger your existing garage door opener (the button you press). No hacking into the opener logic — just simulating a button press.
You need:
- ESP32
- 5V relay module ($5)
- Reed switch to detect if the door is open (optional, $1)
Wire it:
- Relay in+ → 5V (through voltage regulator if powering from GPIO)
- Relay in- → GND
- Relay control → GPIO5
- Relay NO/COM → garage door button terminals (cross the wires in parallel with the physical button)
- Reed switch → GPIO15 (digital input)
Config:
esphome: name: "garage-door"
esp32: board: lolin32
wifi: ssid: !secret wifi_ssid password: !secret wifi_password
api: encryption: key: !secret api_encryption_key
ota: - platform: esphome
binary_sensor: - platform: gpio name: "Garage Door State" pin: GPIO15 device_class: garage_door filters: - invert - delayed_on_off: 500ms
switch: - platform: gpio name: "Garage Door Trigger" pin: GPIO5 restore_mode: RESTORE_DEFAULT_OFF icon: mdi:garage-openIn Home Assistant, create a cover entity to make it feel like a real garage door (with open/close/stop):
cover: - platform: template covers: garage_door: friendly_name: "Garage Door" device_class: garage position_template: "{% if is_state('binary_sensor.garage_door_state', 'on') %}100{% else %}0{% endif %}" open_cover: service: switch.turn_on entity_id: switch.garage_door_trigger close_cover: service: switch.turn_on entity_id: switch.garage_door_triggerNow you can open/close your garage from Home Assistant, your phone, or a voice assistant. And the button on the wall still works.
Project 4: Plant Moisture Monitor (Battery Power + Deep Sleep)
This is where ESPHome gets clever. A battery-powered sensor that reads soil moisture every hour, reports it, then sleeps to save power.
You need:
- ESP32 (not ESP8266; the 8266 doesn’t sleep well)
- Capacitive soil moisture sensor ($8)
- LiPo battery (3.7V, 2000mAh) or CR123A
- Optional: solar trickle charger
Wire it:
- Soil sensor A0 → ADC0 (through voltage divider if needed; most sensors output 0–3V)
- Soil sensor VCC → GPIO2 (power control pin, turns sensor on before reading, off after)
- Battery + → VBAT input
- Battery GND → GND
Config:
esphome: name: "plant-monitor"
esp32: board: lolin32 framework: type: arduino
wifi: ssid: !secret wifi_ssid password: !secret wifi_password power_save_mode: light
api: encryption: key: !secret api_encryption_key
ota: - platform: esphome
deep_sleep: run_duration: 10s sleep_duration: 3600s # 1 hour
switch: - platform: gpio id: sensor_power pin: GPIO2 restore_mode: RESTORE_DEFAULT_OFF
sensor: - platform: adc pin: A0 id: raw_moisture update_interval: never attenuation: 11db - platform: template name: "Soil Moisture" unit_of_measurement: "%" accuracy_decimals: 1 lambda: |- // Turn on sensor, wait, read, turn off id(sensor_power).turn_on(); delay(100); auto raw = id(raw_moisture).sample(); id(sensor_power).turn_off(); // Map 0–4095 ADC to 0–100% (adjust calibration) return (raw / 4095.0) * 100.0; update_interval: 10s
on_boot: then: - component.update: raw_moistureThe trick: deep_sleep puts the ESP32 into a power-saving mode between readings. It wakes up, connects to WiFi, reads the sensor, reports to Home Assistant, then sleeps for an hour. A single CR123A will last 6+ months.
How ESPHome Finds Your Home Assistant
When you reboot an ESPHome device:
- It connects to WiFi.
- It broadcasts a Zeroconf (mDNS) announcement: “I’m
bedroom-sensor.local, speaking ESPHome API on port 6053.” - Home Assistant listens for these announcements and auto-discovers the device.
- Home Assistant shows it in Settings → Devices & Services → Discovered. Click “Configure,” paste the API encryption key, and it’s paired.
No manual IP registration. No MQTT broker setup. No fiddling with hostnames. It just works.
If it doesn’t auto-discover, check:
- Your ESPHome device is on the same WiFi network as Home Assistant.
- Home Assistant’s mDNS is working (
avahi-daemonon Linux, Bonjour on macOS). - The device’s name is unique across your network.
Secrets: Keep Passwords Out of Git
Create a secrets.yaml file in the same directory as your device YAMLs:
wifi_ssid: "MyWiFiNetwork"wifi_password: "supersecretpassword123"api_encryption_key: "base64/abcd1234+/..."ota_password: "another_password"In your device YAML, reference it with !secret:
wifi: ssid: !secret wifi_ssid password: !secret wifi_passwordESPHome inlines the secret at compile time. Add secrets.yaml to .gitignore and check YAML into git without exposing credentials.
Packages: Stop Copy-Pasting Config
If you have 10 ESP32 devices, writing WiFi + API + OTA blocks 10 times is madness. Use packages.
Create a packages/ directory and a base.yaml:
wifi: ssid: !secret wifi_ssid password: !secret wifi_password ap: ssid: "Fallback" password: "fallback123"
api: encryption: key: !secret api_encryption_key
ota: - platform: esphome password: !secret ota_password
web_server: port: 80In your device YAML, include it:
esphome: name: "bedroom-sensor"
packages: base: !include packages/base.yaml
esp32: board: lolin32
sensor: - platform: dht pin: GPIO4 model: DHT22 # ...Now you maintain WiFi + API once. Update packages/base.yaml, and all devices pick it up on the next reflash.
OTA Updates: Reflash Over WiFi
After the first USB flash, you never touch USB again.
From the ESPHome CLI:
esphome run bedroom-sensor.yamlIt compiles, then prompts you to pick the device (auto-discovers on your network) and flashes over WiFi. Done in 30 seconds.
From Home Assistant:
- Edit the YAML in ESPHome → your device name.
- Click the three-dot menu → “Install.”
- Select “Wirelessly” and pick the device.
- Wait 30 seconds.
No USB cables. No power cycling. No “did it brick?”
Web Interface + Captive Portal
If you add a web_server: block, your device runs a simple HTTP server. Visit http://bedroom-sensor.local/ to see sensor readings, toggle switches, and check the log.
Add a captive portal so you can configure WiFi if you forgot the password:
captive_portal:
web_server: port: 80When the device can’t connect to WiFi, it opens a fallback hotspot. Connect to it, visit http://192.168.4.1/, and reconfigure WiFi. No factory reset needed.
ESPHome vs. Tasmota: The Comparison
Tasmota is firmware for ESP8266/ESP32 too. It’s web-UI based: you flash it once, then configure everything through a web dashboard. No compiling.
ESPHome makes you declare hardware in YAML and compile.
Which is better? Depends:
- Tasmota if: You prefer clicking buttons and hate YAML. Tasmota covers most common devices (Shelly relays, Sonoff switches) out of the box.
- ESPHome if: You want declarative config, deep Home Assistant integration, and don’t mind a compile step. It’s friendlier for custom hardware (DHT22 on GPIO4? Done in one line).
I use ESPHome because I like version-controlling my device configs and the Home Assistant API is simpler than MQTT. But Tasmota is solid if you’re not into compiling.
The Real-World Setup
Here’s my actual home setup:
- Bedroom: ESP32 with DHT22 (temperature, humidity) — battery powered, updates every hour, lasts 8 months on a single CR2032.
- Kitchen: ESP32 with motion sensor + light level → controls overhead lights at night automatically.
- Garage: ESP32 with reed switch → garage door state in Home Assistant, plus a relay to trigger the opener.
- Basement: Wemos D1 Mini with capacitive soil sensor on every plant. Reads every 2 hours, alerts if moisture is low.
- Office: ESP32 with CO2 sensor (MH-Z19B) + temperature + humidity. Triggers a window open notification when CO2 spikes.
All of them live in git. All of them update over WiFi. None of them talk to the cloud. Total hardware cost: under $200. Total cloud bill: $0.
Next Steps
- Pick a board. ESP32 for the learner. Cheap, fast, WiFi + Bluetooth. Can’t go wrong.
- Build Project 1. Grab a DHT22, write the YAML, flash it, see it appear in Home Assistant. That “oh, it works!” moment is the whole point.
- Read the ESPHome docs. https://esphome.io/components/ lists every platform (sensor type, switch type, etc.). Pin a bookmark.
- Automate in Home Assistant. The device is just a sensor. Home Assistant orchestrates the logic. That’s the magic.
- Keep it local. Don’t expose your ESPHome devices to the internet directly. Use Home Assistant’s reverse proxy if you want mobile access.
The Honest Truth
ESPHome is not magic. It’s declarative firmware that saves you 500 lines of Arduino C++. It’s good at sensors, switches, and lights. It’s not a full OS (don’t expect Python on it). It’s not a development platform for complex apps.
But if you want to own your smart home — to know exactly what your sensors are doing and where the data goes — ESPHome is the path. No subscriptions. No “sorry, we shut down the cloud service and your device is now a paperweight” email. Just your code, your server, your data.
That’s worth 30 minutes to learn.