ESPHome: Building Custom Sensors for Home Assistant

Turn a three-quid microcontroller into a first-class Home Assistant device with a few lines of YAML

There is a particular joy in solving a problem with three quid of silicon instead of forty quid of someone else’s cloud-tethered gadget. ESPHome is the tool that makes that joy repeatable. You describe a device — its WiFi, its sensors, its outputs — in YAML, ESPHome compiles real C++ firmware, flashes it onto an ESP32 or ESP8266, and the device shows up in Home Assistant automatically with no cloud, no app, and no telemetry leaving your house. I’ve built temperature monitors, soil moisture sensors, a letterbox-open detector, and a CO₂ meter this way, and every one was easier than wiring up the equivalent off-the-shelf product.

Advertisement

ESPHome is firmware-from-config. You write a YAML file per device. Each entry maps to a component: a sensor, a binary sensor, a switch, a display, whatever. ESPHome generates the firmware, and the device speaks the native ESPHome API to Home Assistant over your LAN. Discovery is automatic — flash it, and HA offers to add it.

You run ESPHome either as a Home Assistant add-on (the easy path) or as a standalone Docker container, which is what I do because I like keeping HA lean:

services:
  esphome:
    image: ghcr.io/esphome/esphome:latest
    restart: unless-stopped
    volumes:
      - ./esphome-config:/config
      - /etc/localtime:/etc/localtime:ro
    network_mode: host
    privileged: true   # needed for direct USB flashing

network_mode: host matters: the device discovery and the dashboard both want to see the LAN directly. After the first flash over USB, every subsequent update goes over-the-air.

Here’s a genuinely useful device — a BME280 environmental sensor plus a water-leak probe on a digital pin, on an ESP32:

esphome:
  name: utility-room
  friendly_name: Utility Room

esp32:
  board: esp32dev

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

api:
  encryption:
    key: !secret api_key

ota:
  - platform: esphome
    password: !secret ota_password

logger:

i2c:
  sda: GPIO21
  scl: GPIO22

sensor:
  - platform: bme280_i2c
    address: 0x76
    temperature:
      name: "Utility Temperature"
    humidity:
      name: "Utility Humidity"
    pressure:
      name: "Utility Pressure"
    update_interval: 60s

binary_sensor:
  - platform: gpio
    pin:
      number: GPIO27
      mode:
        input: true
        pullup: true
      inverted: true
    name: "Utility Leak Detected"
    device_class: moisture

The !secret references pull from a shared secrets.yaml, so your WiFi password and API keys never live in the device config — and you can commit the configs to git without leaking credentials. The api.encryption.key is the modern, encrypted native API; the ota block lets you push updates wirelessly forever after the first cable flash.

Compile and flash from the CLI:

$ esphome run utility-room.yaml
INFO Compiling app...
INFO Successfully compiled program.
INFO Detecting upload method...
INFO Resolving IP address of utility-room.local
INFO Connecting to 192.168.1.84
INFO Uploading utility-room.bin (892 kB)
[========================] 100%
INFO OTA successful
INFO Starting log output from 192.168.1.84
[D][bme280:130]: Got temperature=19.4°C humidity=58.1%
[D][binary_sensor:036]: 'Utility Leak Detected': Sending state OFF

Within seconds those entities appear in Home Assistant. Now I can trigger an automation that screams at me if the washing machine springs a leak.

The thing that elevates ESPHome above “dumb sensor” is that it can run logic locally. A button can toggle a relay without a round-trip to HA, so it still works if HA is down:

switch:
  - platform: gpio
    pin: GPIO26
    name: "Utility Heater"
    id: heater

binary_sensor:
  - platform: gpio
    pin:
      number: GPIO25
      mode: { input: true, pullup: true }
      inverted: true
    name: "Heater Button"
    on_press:
      then:
        - switch.toggle: heater

That on_press automation lives on the chip. The relay responds instantly and keeps working during a HA restart or a WiFi blip — local-first behaviour you simply don’t get from cloud gadgets.

It is not all sunshine. You will, at some point, fight a soldering iron, a flaky breadboard jumper, or a sensor with a poorly documented I²C address. ESP8266 boards are cheaper but tighter on memory and pins — for anything non-trivial I default to ESP32. WiFi sensors are power-hungry, so true battery devices need deep-sleep configuration and careful thought, which is fiddlier than the always-powered case shown above. And the first USB flash on Linux occasionally needs you to sort out serial port permissions, which is a five-minute annoyance the docs cover but everyone hits once.

ESPHome is absolutely worth it if you enjoy building things, want to keep your smart home local and private, or have a sensing need no commercial product quite covers. The economics are silly in your favour — a sensor that would cost forty quid as a finished product is a few quid in parts and twenty minutes of YAML. It’s not for someone who wants to unbox a polished retail device and never see a config file; that’s a perfectly valid preference, and ESPHome will feel like homework to them. But if you self-host Home Assistant and have ever thought “I wish I could just measure X” — this is the most rewarding rabbit hole in the hobby.

Advertisement

Related Content

Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.