Skip to content

esphome

tested on simulator

Tier Tier 1
Protocol ESPHome native API over TCP, plaintext or Noise encrypted
Covers ESP32, ESP8266, RP2040 and BK72xx boards running ESPHome, sensors, switches, lights, fans, numbers, selects and buttons defined in ESPHome YAML
Hazard default low
Conformance control
Tested with native API simulator over a local TCP socket, driven by the real aioesphomeapi client

Estimated reach. Every ESPHome node, hundreds of thousands of installs. ESPHome is the most common firmware for DIY ESP-based devices and is preinstalled on a growing set of commercial products under the Made for ESPHome program. Every node speaks the same native API, and the driver maps the common entity types without per-device code.

The esphome driver talks to ESPHome devices over the native API, the same protocol Home Assistant uses. Channels bind to the device's entities by object id, buttons become actions, and every write waits for the device to report the new state.

Driver nameesphome
Libraryaioesphomeapi 46.22.0 (MIT), with protobuf, zeroconf, cryptography and noiseprotocol
Extrauv tool install --compile-bytecode "openhal-hub[esphome] @ git+https://github.com/sohan-bhat/OpenHAL"
Hazard defaultlow
Safe statefrom the device file only
Self-describeyes, from the device's entity list

What it supports

EntityChannelTypeCommand
sensorreadnumber, converted from unit_of_measurement, or stringnone
text_sensorreadstring or enumnone
binary_sensorreadboolnone
switchwriteboolswitch_command
lightwrite <id>boollight_command with state
lightwrite <id>_brightnessnumber, percentlight_command with brightness 0 to 1
fanwrite <id>boolfan_command with state
fanwrite <id>_speednumber, percentfan_command with speed_level out of supported_speed_count
fanwrite <id>_presetenumfan_command with preset_mode
numberwritenumber, converted to unit_of_measurementnumber_command
selectwriteenumselect_command
buttonactionno argumentsbutton_command

A channel binds to the entity whose object id is the channel name, and a name ending in _brightness, _speed or _preset binds to the entity named by the rest of the name. map: { object_id: pump_seconds, attribute: state } sets both explicitly. Bindings are checked against the device's entity list on every connect, and a missing entity fails the connect with the list of entities the device has.

Because native API commands have no acknowledgement, the driver verifies each write by waiting up to timeout after the command for the state update that shows the new value. A device that doesn't report the new value fails the write, and a later read shows what the device really reports. Fan speeds snap to the nearest supported level, and the write's reading is the speed the fan really runs at.

match.esphome holds glob patterns for device info fields such as name and model, checked on connect.

Setup

  1. Install the extra with uv tool install --compile-bytecode "openhal-hub[esphome] @ git+https://github.com/sohan-bhat/OpenHAL".
  2. Give the node an api: block in its YAML. With encryption: key: ..., put the same base64 key in an environment variable such as PLANT_PSK and set noise_psk_env.
  3. Prefer failsafes on the node itself, for example an on_turn_on automation that turns a pump off after a delay, or api: reboot_timeout. Because the hub can't detect those failsafes, mention them in safe_state_note.
  4. Start from openhal/drivers/esphome/examples/plant-waterer.hal.yaml or from describe().
  5. Validate with openhal file validate, then confirm the limits by hand.

Parameters

KeyDefaultMeaning
hostrequiredHost name, .local name or address
port6053Native API port
noise_psk_envnoneEnvironment variable that holds the API encryption key
password_envnoneEnvironment variable that holds the legacy API password
timeout5 sHandshake deadline, and how long a write waits for the new state

Example device file

device: Plant waterer
id: plant
match: { mdns: _esphomelib._tcp, esphome: { name: "plant-*" } }
connect: { esphome: plant-node.local, port: 6053, noise_psk_env: PLANT_PSK }
read:
  soil_moisture: { unit: percent, every: 10 s, range: [0, 100] }
write:
  pump: { type: bool }
safe_state: { pump: false }
watchdog:
  max_duration:
    - when: "pump == true"
      for: 20 s
hazard: low

Known limitations

  • Covers, climate, locks, valves, media players, alarm panels and user-defined services are not mapped yet.
  • RGB and white channels of lights are not exposed, only state and brightness.
  • A node in deep sleep is unreachable most of the time, which the hub reports as disconnected.
  • The native API allows one Home Assistant and several other clients. Commands from Home Assistant automations still reach the device and are outside OpenHAL's limits.

Hazard defaults

The default hazard is low, because most nodes switch lights, fans, pumps and relays at low voltage, but a relay that switches mains to a heater deserves medium or high in the device file.

Sources