Skip to content
Go back

ESPHome: DIY IoT Without Cloud

By SumGuy 14 min read
ESPHome: DIY IoT Without Cloud

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:

  1. 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.

  2. ESPHome: Either the web-based dashboard (runs on your Home Assistant instance or standalone) or the CLI tool on your laptop.

  3. 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)

Terminal window
pip install esphome
esphome dashboard ~/esphome

This 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:

bedroom-sensor.yaml
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_OFF

Line by line:


Project 1: Bedroom Temp/Humidity Sensor (The Warm-Up)

You need:

Wire it:

Config:

temp-humidity.yaml
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: 60s

Flash 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:

Wire it:

Config:

motion-light.yaml
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: GPIO5

Now 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_lights

The 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:

Wire it:

Config:

garage-door.yaml
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-open

In 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_trigger

Now 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:

Wire it:

Config:

plant-monitor.yaml
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_moisture

The 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:

  1. It connects to WiFi.
  2. It broadcasts a Zeroconf (mDNS) announcement: “I’m bedroom-sensor.local, speaking ESPHome API on port 6053.”
  3. Home Assistant listens for these announcements and auto-discovers the device.
  4. 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:


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_password

ESPHome 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:

packages/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: 80

In your device YAML, include it:

bedroom-sensor.yaml
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:

Terminal window
esphome run bedroom-sensor.yaml

It compiles, then prompts you to pick the device (auto-discovers on your network) and flashes over WiFi. Done in 30 seconds.

From Home Assistant:

  1. Edit the YAML in ESPHome → your device name.
  2. Click the three-dot menu → “Install.”
  3. Select “Wirelessly” and pick the device.
  4. 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: 80

When 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:

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:

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

  1. Pick a board. ESP32 for the learner. Cheap, fast, WiFi + Bluetooth. Can’t go wrong.
  2. 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.
  3. Read the ESPHome docs. https://esphome.io/components/ lists every platform (sensor type, switch type, etc.). Pin a bookmark.
  4. Automate in Home Assistant. The device is just a sensor. Home Assistant orchestrates the logic. That’s the magic.
  5. 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.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
ddrescue vs TestDisk vs PhotoRec

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts